From 5a1ed4c94385b9d2a3f6b711485f2c45360c0b81 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i@maskray.me>
Date: Sun, 7 Oct 2018 22:02:28 -0700
Subject: [PATCH] Support workspace folders

---
 CMakeLists.txt                                |   2 +-
 src/clang_tu.cc                               |   4 +-
 src/config.h                                  |   5 +-
 src/include_complete.cc                       |  59 +++----
 src/lsp.h                                     |   6 +
 src/message_handler.cc                        |  15 +-
 src/messages/ccls_info.cc                     |   4 +-
 src/messages/initialize.cc                    |  45 ++++--
 src/messages/workspace_did.cc                 | 108 +++++++++++++
 .../workspace_didChangeConfiguration.cc       |  49 ------
 src/pipeline.cc                               |  28 ++--
 src/project.cc                                | 150 ++++++++++--------
 src/project.h                                 |  17 +-
 13 files changed, 300 insertions(+), 192 deletions(-)
 create mode 100644 src/messages/workspace_did.cc
 delete mode 100644 src/messages/workspace_didChangeConfiguration.cc

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 47af70f0..29a3008f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -231,7 +231,7 @@ target_sources(ccls PRIVATE
   src/messages/textDocument_rename.cc
   src/messages/textDocument_signatureHelp.cc
   src/messages/textDocument_typeDefinition.cc
-  src/messages/workspace_didChangeConfiguration.cc
+  src/messages/workspace_did.cc
   src/messages/workspace_didChangeWatchedFiles.cc
   src/messages/workspace_symbol.cc
 )
diff --git a/src/clang_tu.cc b/src/clang_tu.cc
index b58a69e9..d1dd3f02 100644
--- a/src/clang_tu.cc
+++ b/src/clang_tu.cc
@@ -30,7 +30,9 @@ std::string PathFromFileEntry(const FileEntry &file) {
     Name = file.getName();
   std::string ret = NormalizePath(Name);
   // Resolve /usr/include/c++/7.3.0 symlink.
-  if (!StartsWith(ret, g_config->projectRoot)) {
+  if (!llvm::any_of(g_config->workspaceFolders, [&](const std::string &root) {
+        return StartsWith(ret, root);
+      })) {
     SmallString<256> dest;
     llvm::sys::fs::real_path(ret, dest);
     ret = llvm::sys::path::convert_to_slash(dest.str());
diff --git a/src/config.h b/src/config.h
index abb31dee..b358cb67 100644
--- a/src/config.h
+++ b/src/config.h
@@ -29,8 +29,9 @@ initialization options specified by the client. For example, in shell syntax:
   '--init={"index": {"comments": 2, "whitelist": ["."]}}'
 */
 struct Config {
-  // Root directory of the project. **Not available for configuration**
-  std::string projectRoot;
+  // **Not available for configuration**
+  std::string fallbackFolder;
+  std::vector<std::string> workspaceFolders;
   // If specified, this option overrides compile_commands.json and this
   // external command will be executed with an option |projectRoot|.
   // The initialization options will be provided as stdin.
diff --git a/src/include_complete.cc b/src/include_complete.cc
index cf220a56..c7539849 100644
--- a/src/include_complete.cc
+++ b/src/include_complete.cc
@@ -59,23 +59,27 @@ size_t TrimCommonPathPrefix(const std::string &result,
 }
 
 // Returns true iff angle brackets should be used.
-bool TrimPath(Project *project, const std::string &project_root,
-              std::string *insert_path) {
-  size_t start = TrimCommonPathPrefix(*insert_path, project_root);
+bool TrimPath(Project *project, std::string &path) {
+  size_t pos = 0;
   bool angle = false;
-
-  for (auto &include_dir : project->quote_include_directories)
-    start = std::max(start, TrimCommonPathPrefix(*insert_path, include_dir));
-
-  for (auto &include_dir : project->angle_include_directories) {
-    auto len = TrimCommonPathPrefix(*insert_path, include_dir);
-    if (len > start) {
-      start = len;
+  for (auto &[root, folder] : project->root2folder) {
+    size_t pos1 = 0;
+    for (auto &search : folder.angle_search_list)
+      pos1 = std::max(pos1, TrimCommonPathPrefix(path, search));
+    if (pos1 > pos) {
+      pos = pos1;
       angle = true;
     }
-  }
 
-  *insert_path = insert_path->substr(start);
+    pos1 = TrimCommonPathPrefix(path, root);
+    for (auto &search : folder.quote_search_list)
+      pos1 = std::max(pos1, TrimCommonPathPrefix(path, search));
+    if (pos1 > pos) {
+      pos = pos1;
+      angle = false;
+    }
+  }
+  path = path.substr(pos);
   return angle;
 }
 
@@ -119,13 +123,15 @@ void IncludeComplete::Rescan() {
   is_scanning = true;
   std::thread([this]() {
     set_thread_name("include");
-    Timer timer("include", "scan include paths");
-    TimeRegion region(timer);
-
-    for (const std::string &dir : project_->quote_include_directories)
-      InsertIncludesFromDirectory(dir, false /*use_angle_brackets*/);
-    for (const std::string &dir : project_->angle_include_directories)
-      InsertIncludesFromDirectory(dir, true /*use_angle_brackets*/);
+    std::unordered_set<std::string> angle_set, quote_set;
+    for (auto &[root, folder] : project_->root2folder) {
+      for (const std::string &search : folder.angle_search_list)
+        if (angle_set.insert(search).second)
+          InsertIncludesFromDirectory(search, true);
+      for (const std::string &search : folder.quote_search_list)
+        if (quote_set.insert(search).second)
+          InsertIncludesFromDirectory(search, false);
+    }
 
     is_scanning = false;
   })
@@ -152,22 +158,21 @@ void IncludeComplete::InsertCompletionItem(const std::string &absolute_path,
   }
 }
 
-void IncludeComplete::AddFile(const std::string &absolute_path) {
-  if (!EndsWithAny(absolute_path, g_config->completion.include.suffixWhitelist))
+void IncludeComplete::AddFile(const std::string &path) {
+  if (!EndsWithAny(path, g_config->completion.include.suffixWhitelist))
     return;
-  if (match_ && !match_->IsMatch(absolute_path))
+  if (match_ && !match_->IsMatch(path))
     return;
 
-  std::string trimmed_path = absolute_path;
-  bool use_angle_brackets =
-      TrimPath(project_, g_config->projectRoot, &trimmed_path);
+  std::string trimmed_path = path;
+  bool use_angle_brackets = TrimPath(project_, trimmed_path);
   lsCompletionItem item =
       BuildCompletionItem(trimmed_path, use_angle_brackets, false /*is_stl*/);
 
   std::unique_lock<std::mutex> lock(completion_items_mutex, std::defer_lock);
   if (is_scanning)
     lock.lock();
-  InsertCompletionItem(absolute_path, std::move(item));
+  InsertCompletionItem(path, std::move(item));
 }
 
 void IncludeComplete::InsertIncludesFromDirectory(std::string directory,
diff --git a/src/lsp.h b/src/lsp.h
index dbe73984..aa8029dd 100644
--- a/src/lsp.h
+++ b/src/lsp.h
@@ -307,6 +307,12 @@ struct lsTextDocumentDidChangeParams {
 MAKE_REFLECT_STRUCT(lsTextDocumentDidChangeParams, textDocument,
                     contentChanges);
 
+struct lsWorkspaceFolder {
+  lsDocumentUri uri;
+  std::string name;
+};
+MAKE_REFLECT_STRUCT(lsWorkspaceFolder, uri, name);
+
 // Show a message to the user.
 enum class lsMessageType : int { Error = 1, Warning = 2, Info = 3, Log = 4 };
 MAKE_REFLECT_TYPE_PROXY(lsMessageType)
diff --git a/src/message_handler.cc b/src/message_handler.cc
index 1b699ed0..ddffa25c 100644
--- a/src/message_handler.cc
+++ b/src/message_handler.cc
@@ -90,27 +90,24 @@ bool FindFileOrFail(DB *db, Project *project, std::optional<lsRequestId> id,
   if (out_file_id)
     *out_file_id = -1;
 
-  bool indexing;
+  bool has_entry = false;
   {
     std::lock_guard<std::mutex> lock(project->mutex_);
-    indexing = project->path_to_entry_index.find(absolute_path) !=
-               project->path_to_entry_index.end();
+    for (auto &[root, folder] : project->root2folder)
+      has_entry |= folder.path2entry_index.count(absolute_path);
   }
-  if (indexing)
-    LOG_S(INFO) << "\"" << absolute_path << "\" is being indexed.";
-  else
-    LOG_S(INFO) << "unable to find file \"" << absolute_path << "\"";
 
   if (id) {
     Out_Error out;
     out.id = *id;
-    if (indexing) {
+    if (has_entry) {
       out.error.code = lsErrorCodes::ServerNotInitialized;
-      out.error.message = absolute_path + " is being indexed.";
+      out.error.message = absolute_path + " is being indexed";
     } else {
       out.error.code = lsErrorCodes::InternalError;
       out.error.message = "Unable to find file " + absolute_path;
     }
+    LOG_S(INFO) << out.error.message;
     pipeline::WriteStdout(kMethodType_Unknown, out);
   }
 
diff --git a/src/messages/ccls_info.cc b/src/messages/ccls_info.cc
index 3efcae50..8aa99f8b 100644
--- a/src/messages/ccls_info.cc
+++ b/src/messages/ccls_info.cc
@@ -61,7 +61,9 @@ struct Handler_cclsInfo : BaseMessageHandler<In_cclsInfo> {
     out.result.db.types = db->types.size();
     out.result.db.vars = db->vars.size();
     out.result.pipeline.pendingIndexRequests = pipeline::pending_index_requests;
-    out.result.project.entries = project->entries.size();
+    out.result.project.entries = 0;
+    for (auto &[_, folder] : project->root2folder)
+      out.result.project.entries += folder.entries.size();
     pipeline::WriteStdout(cclsInfo, out);
   }
 };
diff --git a/src/messages/initialize.cc b/src/messages/initialize.cc
index cd9d378e..a7e6bd41 100644
--- a/src/messages/initialize.cc
+++ b/src/messages/initialize.cc
@@ -176,8 +176,17 @@ struct lsServerCapabilities {
   struct ExecuteCommandOptions {
     std::vector<std::string> commands{std::string(ccls_xref)};
   } executeCommandProvider;
+  struct Workspace {
+    struct WorkspaceFolders {
+      bool supported = true;
+      bool changeNotifications = true;
+    } workspaceFolders;
+  } workspace;
 };
 MAKE_REFLECT_STRUCT(lsServerCapabilities::ExecuteCommandOptions, commands);
+MAKE_REFLECT_STRUCT(lsServerCapabilities::Workspace::WorkspaceFolders,
+                    supported, changeNotifications);
+MAKE_REFLECT_STRUCT(lsServerCapabilities::Workspace, workspaceFolders);
 MAKE_REFLECT_STRUCT(lsServerCapabilities, textDocumentSync, hoverProvider,
                     completionProvider, signatureHelpProvider,
                     definitionProvider, implementationProvider,
@@ -187,7 +196,7 @@ MAKE_REFLECT_STRUCT(lsServerCapabilities, textDocumentSync, hoverProvider,
                     codeLensProvider, documentFormattingProvider,
                     documentRangeFormattingProvider,
                     documentOnTypeFormattingProvider, renameProvider,
-                    documentLinkProvider, executeCommandProvider);
+                    documentLinkProvider, executeCommandProvider, workspace);
 
 // Workspace specific client capabilities.
 struct lsWorkspaceClientCapabilites {
@@ -345,6 +354,8 @@ struct lsInitializeParams {
 
   // The initial trace setting. If omitted trace is disabled ('off').
   lsTrace trace = lsTrace::Off;
+
+  std::vector<lsWorkspaceFolder> workspaceFolders;
 };
 
 void Reflect(Reader &reader, lsInitializeParams::lsTrace &value) {
@@ -378,7 +389,8 @@ void Reflect(Writer& writer, lsInitializeParams::lsTrace& value) {
 #endif
 
 MAKE_REFLECT_STRUCT(lsInitializeParams, processId, rootPath, rootUri,
-                    initializationOptions, capabilities, trace);
+                    initializationOptions, capabilities, trace,
+                    workspaceFolders);
 
 struct lsInitializeError {
   // Indicates whether the client should retry to send the
@@ -484,19 +496,28 @@ struct Handler_Initialize : BaseMessageHandler<In_InitializeRequest> {
 
     // Set project root.
     EnsureEndsInSlash(project_path);
-    g_config->projectRoot = project_path;
-    if (g_config->cacheDirectory.size()) {
-      // Create two cache directories for files inside and outside of the
-      // project.
-      auto len = g_config->projectRoot.size();
-      std::string escaped = EscapeFileName(g_config->projectRoot.substr(0, len - 1));
-      sys::fs::create_directories(g_config->cacheDirectory + escaped);
-      sys::fs::create_directories(g_config->cacheDirectory + '@' + escaped);
+    g_config->fallbackFolder = project_path;
+    for (const lsWorkspaceFolder &wf : request->params.workspaceFolders) {
+      std::string path = wf.uri.GetPath();
+      EnsureEndsInSlash(path);
+      g_config->workspaceFolders.push_back(path);
+      LOG_S(INFO) << "add workspace folder " << wf.name << ": " << path;
     }
+    if (request->params.workspaceFolders.empty())
+      g_config->workspaceFolders.push_back(project_path);
+    if (g_config->cacheDirectory.size())
+      for (const std::string &folder : g_config->workspaceFolders) {
+        // Create two cache directories for files inside and outside of the
+        // project.
+        std::string escaped =
+            EscapeFileName(folder.substr(0, folder.size() - 1));
+        sys::fs::create_directories(g_config->cacheDirectory + escaped);
+        sys::fs::create_directories(g_config->cacheDirectory + '@' + escaped);
+      }
 
     idx::Init();
-
-    project->Load(project_path);
+    for (const std::string &folder : g_config->workspaceFolders)
+      project->Load(folder);
 
     // Start indexer threads. Start this after loading the project, as that
     // may take a long time. Indexer threads will emit status/progress
diff --git a/src/messages/workspace_did.cc b/src/messages/workspace_did.cc
new file mode 100644
index 00000000..949436f5
--- /dev/null
+++ b/src/messages/workspace_did.cc
@@ -0,0 +1,108 @@
+/* Copyright 2017-2018 ccls Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+#include "clang_complete.hh"
+#include "log.hh"
+#include "message_handler.h"
+#include "pipeline.hh"
+#include "project.h"
+#include "working_files.h"
+
+#include <llvm/ADT/STLExtras.h>
+using namespace ccls;
+
+namespace {
+MethodType didChangeConfiguration = "workspace/didChangeConfiguration",
+           didChangeWorkspaceFolders = "workspace/didChangeWorkspaceFolders";
+
+struct lsDidChangeConfigurationParams {
+  bool placeholder;
+};
+MAKE_REFLECT_STRUCT(lsDidChangeConfigurationParams, placeholder);
+
+struct In_workspaceDidChangeConfiguration : public NotificationInMessage {
+  MethodType GetMethodType() const override { return didChangeConfiguration; }
+  lsDidChangeConfigurationParams params;
+};
+MAKE_REFLECT_STRUCT(In_workspaceDidChangeConfiguration, params);
+REGISTER_IN_MESSAGE(In_workspaceDidChangeConfiguration);
+
+struct Handler_workspaceDidChangeConfiguration
+    : BaseMessageHandler<In_workspaceDidChangeConfiguration> {
+  MethodType GetMethodType() const override { return didChangeConfiguration; }
+  void Run(In_workspaceDidChangeConfiguration *request) override {
+    for (const std::string &folder : g_config->workspaceFolders)
+      project->Load(folder);
+
+    project->Index(working_files, lsRequestId());
+
+    clang_complete->FlushAllSessions();
+  }
+};
+REGISTER_MESSAGE_HANDLER(Handler_workspaceDidChangeConfiguration);
+
+struct lsWorkspaceFoldersChangeEvent {
+  std::vector<lsWorkspaceFolder> added, removed;
+};
+MAKE_REFLECT_STRUCT(lsWorkspaceFoldersChangeEvent, added, removed);
+
+struct In_workspaceDidChangeWorkspaceFolders : public NotificationInMessage {
+  MethodType GetMethodType() const override {
+    return didChangeWorkspaceFolders;
+  }
+  struct Params {
+    lsWorkspaceFoldersChangeEvent event;
+  } params;
+};
+MAKE_REFLECT_STRUCT(In_workspaceDidChangeWorkspaceFolders::Params, event);
+MAKE_REFLECT_STRUCT(In_workspaceDidChangeWorkspaceFolders, params);
+REGISTER_IN_MESSAGE(In_workspaceDidChangeWorkspaceFolders);
+
+struct Handler_workspaceDidChangeWorkspaceFolders
+    : BaseMessageHandler<In_workspaceDidChangeWorkspaceFolders> {
+  MethodType GetMethodType() const override {
+    return didChangeWorkspaceFolders;
+  }
+  void Run(In_workspaceDidChangeWorkspaceFolders *request) override {
+    const auto &event = request->params.event;
+    for (const lsWorkspaceFolder &wf : event.removed) {
+      std::string root = wf.uri.GetPath();
+      EnsureEndsInSlash(root);
+      LOG_S(INFO) << "delete workspace folder " << wf.name << ": " << root;
+      auto it = llvm::find(g_config->workspaceFolders, root);
+      if (it != g_config->workspaceFolders.end()) {
+        g_config->workspaceFolders.erase(it);
+        {
+          // auto &folder = project->root2folder[path];
+          // FIXME delete
+        }
+        project->root2folder.erase(root);
+      }
+    }
+    for (const lsWorkspaceFolder &wf : event.added) {
+      std::string root = wf.uri.GetPath();
+      EnsureEndsInSlash(root);
+      LOG_S(INFO) << "add workspace folder " << wf.name << ": " << root;
+      g_config->workspaceFolders.push_back(root);
+      project->Load(root);
+    }
+
+    project->Index(working_files, lsRequestId());
+
+    clang_complete->FlushAllSessions();
+  }
+};
+REGISTER_MESSAGE_HANDLER(Handler_workspaceDidChangeWorkspaceFolders);
+} // namespace
diff --git a/src/messages/workspace_didChangeConfiguration.cc b/src/messages/workspace_didChangeConfiguration.cc
deleted file mode 100644
index 9b9ba474..00000000
--- a/src/messages/workspace_didChangeConfiguration.cc
+++ /dev/null
@@ -1,49 +0,0 @@
-/* Copyright 2017-2018 ccls Authors
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-==============================================================================*/
-
-#include "clang_complete.hh"
-#include "message_handler.h"
-#include "pipeline.hh"
-#include "project.h"
-#include "working_files.h"
-using namespace ccls;
-
-namespace {
-MethodType kMethodType = "workspace/didChangeConfiguration";
-
-struct lsDidChangeConfigurationParams {
-  bool placeholder;
-};
-MAKE_REFLECT_STRUCT(lsDidChangeConfigurationParams, placeholder);
-
-struct In_WorkspaceDidChangeConfiguration : public NotificationInMessage {
-  MethodType GetMethodType() const override { return kMethodType; }
-  lsDidChangeConfigurationParams params;
-};
-MAKE_REFLECT_STRUCT(In_WorkspaceDidChangeConfiguration, params);
-REGISTER_IN_MESSAGE(In_WorkspaceDidChangeConfiguration);
-
-struct Handler_WorkspaceDidChangeConfiguration
-    : BaseMessageHandler<In_WorkspaceDidChangeConfiguration> {
-  MethodType GetMethodType() const override { return kMethodType; }
-  void Run(In_WorkspaceDidChangeConfiguration *request) override {
-    project->Load(g_config->projectRoot);
-    project->Index(working_files, lsRequestId());
-
-    clang_complete->FlushAllSessions();
-  }
-};
-REGISTER_MESSAGE_HANDLER(Handler_WorkspaceDidChangeConfiguration);
-} // namespace
diff --git a/src/pipeline.cc b/src/pipeline.cc
index c61469c0..f230413d 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -128,18 +128,17 @@ std::string AppendSerializationFormat(const std::string &base) {
 }
 
 std::string GetCachePath(const std::string &source_file) {
-  std::string cache_file;
-  auto len = g_config->projectRoot.size();
-  if (StartsWith(source_file, g_config->projectRoot)) {
-    cache_file = EscapeFileName(g_config->projectRoot.substr(0, len - 1)) + '/' +
-                 EscapeFileName(source_file.substr(len));
-  } else {
-    cache_file = '@' +
-                 EscapeFileName(g_config->projectRoot.substr(0, len - 1)) + '/' +
-                 EscapeFileName(source_file);
-  }
-
-  return g_config->cacheDirectory + cache_file;
+  for (auto &root : g_config->workspaceFolders)
+    if (StartsWith(source_file, root)) {
+      auto len = root.size();
+      return g_config->cacheDirectory +
+             EscapeFileName(root.substr(0, len - 1)) + '/' +
+             EscapeFileName(source_file.substr(len));
+    }
+  return g_config->cacheDirectory + '@' +
+         EscapeFileName(g_config->fallbackFolder.substr(
+             0, g_config->fallbackFolder.size() - 1)) +
+         '/' + EscapeFileName(source_file);
 }
 
 std::unique_ptr<IndexFile> RawCacheLoad(const std::string &path) {
@@ -280,7 +279,7 @@ bool Indexer_Parse(CompletionManager *completion, WorkingFiles *wfiles,
           request.mode != IndexMode::NonInteractive);
         if (entry.id >= 0) {
           std::lock_guard lock2(project->mutex_);
-          project->path_to_entry_index[path] = entry.id;
+          project->root2folder[entry.root].path2entry_index[path] = entry.id;
         }
       }
       return true;
@@ -343,8 +342,9 @@ bool Indexer_Parse(CompletionManager *completion, WorkingFiles *wfiles,
       }
       if (entry.id >= 0) {
         std::lock_guard<std::mutex> lock(project->mutex_);
+        auto &folder = project->root2folder[entry.root];
         for (auto &dep : curr->dependencies)
-          project->path_to_entry_index[dep.first.val().str()] = entry.id;
+          folder.path2entry_index[dep.first.val().str()] = entry.id;
       }
     }
   }
diff --git a/src/project.cc b/src/project.cc
index 1278e1e4..7968b5bb 100644
--- a/src/project.cc
+++ b/src/project.cc
@@ -54,7 +54,7 @@ enum class ProjectMode { CompileCommandsJson, DotCcls, ExternalCommand };
 struct ProjectConfig {
   std::unordered_set<std::string> quote_dirs;
   std::unordered_set<std::string> angle_dirs;
-  std::string project_dir;
+  std::string root;
   ProjectMode mode = ProjectMode::CompileCommandsJson;
 };
 
@@ -183,7 +183,7 @@ std::vector<Project::Entry> LoadFromDirectoryListing(ProjectConfig *config) {
   std::vector<Project::Entry> result;
   config->mode = ProjectMode::DotCcls;
   SmallString<256> Path;
-  sys::path::append(Path, config->project_dir, ".ccls");
+  sys::path::append(Path, config->root, ".ccls");
   LOG_IF_S(WARNING, !sys::fs::exists(Path) && g_config->clang.extraArgs.empty())
       << "ccls has no clang arguments. Use either "
          "compile_commands.json or .ccls, See ccls README for "
@@ -191,9 +191,9 @@ std::vector<Project::Entry> LoadFromDirectoryListing(ProjectConfig *config) {
 
   std::unordered_map<std::string, std::vector<const char *>> folder_args;
   std::vector<std::string> files;
-  const std::string &project_dir = config->project_dir;
+  const std::string &root = config->root;
 
-  GetFilesInFolder(project_dir, true /*recursive*/,
+  GetFilesInFolder(root, true /*recursive*/,
                    true /*add_folder_to_path*/,
                    [&folder_args, &files](const std::string &path) {
                      if (SourceFileLanguage(path) != LanguageId::Unknown) {
@@ -211,7 +211,7 @@ std::vector<Project::Entry> LoadFromDirectoryListing(ProjectConfig *config) {
                      }
                    });
 
-  auto GetCompilerArgumentForFile = [&project_dir,
+  auto GetCompilerArgumentForFile = [&root,
                                      &folder_args](std::string cur) {
     while (!(cur = sys::path::parent_path(cur)).empty()) {
       auto it = folder_args.find(cur);
@@ -219,17 +219,18 @@ std::vector<Project::Entry> LoadFromDirectoryListing(ProjectConfig *config) {
         return it->second;
       std::string normalized = NormalizePath(cur);
       // Break if outside of the project root.
-      if (normalized.size() <= project_dir.size() ||
-          normalized.compare(0, project_dir.size(), project_dir) != 0)
+      if (normalized.size() <= root.size() ||
+          normalized.compare(0, root.size(), root) != 0)
         break;
     }
-    return folder_args[project_dir];
+    return folder_args[root];
   };
 
   ProjectProcessor proc(config);
   for (const std::string &file : files) {
     Project::Entry e;
-    e.directory = config->project_dir;
+    e.root = config->root;
+    e.directory = config->root;
     e.filename = file;
     e.args = GetCompilerArgumentForFile(file);
     if (e.args.empty())
@@ -247,7 +248,7 @@ LoadEntriesFromDirectory(ProjectConfig *project,
                          const std::string &opt_compdb_dir) {
   // If there is a .ccls file always load using directory listing.
   SmallString<256> Path;
-  sys::path::append(Path, project->project_dir, ".ccls");
+  sys::path::append(Path, project->root, ".ccls");
   if (sys::fs::exists(Path))
     return LoadFromDirectoryListing(project);
 
@@ -257,8 +258,7 @@ LoadEntriesFromDirectory(ProjectConfig *project,
   if (g_config->compilationDatabaseCommand.empty()) {
     project->mode = ProjectMode::CompileCommandsJson;
     // Try to load compile_commands.json, but fallback to a project listing.
-    comp_db_dir =
-        opt_compdb_dir.empty() ? project->project_dir : opt_compdb_dir;
+    comp_db_dir = opt_compdb_dir.empty() ? project->root : opt_compdb_dir;
     sys::path::append(Path, comp_db_dir, "compile_commands.json");
   } else {
     project->mode = ProjectMode::ExternalCommand;
@@ -276,7 +276,7 @@ LoadEntriesFromDirectory(ProjectConfig *project,
     Reflect(json_writer, *g_config);
     std::string contents = GetExternalCommandOutput(
         std::vector<std::string>{g_config->compilationDatabaseCommand,
-                                 project->project_dir},
+                                 project->root},
         input.GetString());
     FILE *fout = fopen(Path.c_str(), "wb");
     fwrite(contents.c_str(), contents.size(), 1, fout);
@@ -307,6 +307,8 @@ LoadEntriesFromDirectory(ProjectConfig *project,
   ProjectProcessor proc(project);
   for (tooling::CompileCommand &Cmd : CDB->getAllCompileCommands()) {
     Project::Entry entry;
+    entry.root = project->root;
+    DoPathMapping(entry.root);
     entry.directory = NormalizePath(Cmd.Directory);
     DoPathMapping(entry.directory);
     entry.filename =
@@ -342,77 +344,78 @@ int ComputeGuessScore(std::string_view a, std::string_view b) {
 
 } // namespace
 
-void Project::Load(const std::string &root_directory) {
+void Project::Load(const std::string &root) {
+  assert(root.back() == '/');
   ProjectConfig project;
-  project.project_dir = root_directory;
-  entries = LoadEntriesFromDirectory(&project,
-                                     g_config->compilationDatabaseDirectory);
+  project.root = root;
+  Folder &folder = root2folder[root];
 
-  // Cleanup / postprocess include directories.
-  quote_include_directories.assign(project.quote_dirs.begin(),
-                                   project.quote_dirs.end());
-  angle_include_directories.assign(project.angle_dirs.begin(),
-                                   project.angle_dirs.end());
-  for (std::string &path : quote_include_directories) {
+  folder.entries = LoadEntriesFromDirectory(
+      &project, g_config->compilationDatabaseDirectory);
+  folder.quote_search_list.assign(project.quote_dirs.begin(),
+                                  project.quote_dirs.end());
+  folder.angle_search_list.assign(project.angle_dirs.begin(),
+                                  project.angle_dirs.end());
+  for (std::string &path : folder.angle_search_list) {
     EnsureEndsInSlash(path);
-    LOG_S(INFO) << "quote_include_dir: " << path;
+    LOG_S(INFO) << "angle search: " << path;
   }
-  for (std::string &path : angle_include_directories) {
+  for (std::string &path : folder.quote_search_list) {
     EnsureEndsInSlash(path);
-    LOG_S(INFO) << "angle_include_dir: " << path;
+    LOG_S(INFO) << "quote search: " << path;
   }
 
   // Setup project entries.
   std::lock_guard lock(mutex_);
-  path_to_entry_index.reserve(entries.size());
-  for (size_t i = 0; i < entries.size(); ++i) {
-    entries[i].id = i;
-    path_to_entry_index[entries[i].filename] = i;
+  folder.path2entry_index.reserve(folder.entries.size());
+  for (size_t i = 0; i < folder.entries.size(); ++i) {
+    folder.entries[i].id = i;
+    folder.path2entry_index[folder.entries[i].filename] = i;
   }
 }
 
 void Project::SetArgsForFile(const std::vector<const char *> &args,
                              const std::string &path) {
   std::lock_guard<std::mutex> lock(mutex_);
-  auto it = path_to_entry_index.find(path);
-  if (it != path_to_entry_index.end()) {
-    // The entry already exists in the project, just set the flags.
-    this->entries[it->second].args = args;
-  } else {
-    // Entry wasn't found, so we create a new one.
-    Entry entry;
-    entry.is_inferred = false;
-    entry.filename = path;
-    entry.args = args;
-    this->entries.emplace_back(entry);
+  for (auto &[root, folder] : root2folder) {
+    auto it = folder.path2entry_index.find(path);
+    if (it != folder.path2entry_index.end()) {
+      // The entry already exists in the project, just set the flags.
+      folder.entries[it->second].args = args;
+      return;
+    }
   }
 }
 
 Project::Entry Project::FindEntry(const std::string &path,
                                   bool can_be_inferred) {
-  {
-    std::lock_guard<std::mutex> lock(mutex_);
-    auto it = path_to_entry_index.find(path);
-    if (it != path_to_entry_index.end()) {
-      Project::Entry &entry = entries[it->second];
+  std::lock_guard<std::mutex> lock(mutex_);
+  for (auto &[root, folder] : root2folder) {
+    auto it = folder.path2entry_index.find(path);
+    if (it != folder.path2entry_index.end()) {
+      Project::Entry &entry = folder.entries[it->second];
       if (can_be_inferred || entry.filename == path)
         return entry;
     }
   }
 
-  // We couldn't find the file. Try to infer it.
-  // TODO: Cache inferred file in a separate array (using a lock or similar)
-  Entry *best_entry = nullptr;
-  int best_score = INT_MIN;
-  for (Entry &entry : entries) {
-    int score = ComputeGuessScore(path, entry.filename);
-    if (score > best_score) {
-      best_score = score;
-      best_entry = &entry;
-    }
-  }
-
   Project::Entry result;
+  const Entry *best_entry = nullptr;
+  int best_score = INT_MIN;
+  for (auto &[root, folder] : root2folder) {
+    for (const Entry &entry : folder.entries) {
+      int score = ComputeGuessScore(path, entry.filename);
+      if (score > best_score) {
+        best_score = score;
+        best_entry = &entry;
+      }
+    }
+    if (StartsWith(path, root))
+      result.root = root;
+  }
+  if (result.root.empty())
+    result.root = g_config->fallbackFolder;
+
   result.is_inferred = true;
   result.filename = path;
   if (!best_entry) {
@@ -442,18 +445,25 @@ void Project::Index(WorkingFiles *wfiles, lsRequestId id) {
   auto &gi = g_config->index;
   GroupMatch match(gi.whitelist, gi.blacklist),
       match_i(gi.initialWhitelist, gi.initialBlacklist);
-  for (int i = 0; i < entries.size(); ++i) {
-    const Project::Entry &entry = entries[i];
-    std::string reason;
-    if (match.IsMatch(entry.filename, &reason) &&
-        match_i.IsMatch(entry.filename, &reason)) {
-      bool interactive = wfiles->GetFileByFilename(entry.filename) != nullptr;
-      pipeline::Index(
-          entry.filename, entry.args,
-          interactive ? IndexMode::Normal : IndexMode::NonInteractive, id);
-    } else {
-      LOG_V(1) << "[" << i << "/" << entries.size() << "]: " << reason
-               << "; skip " << entry.filename;
+  {
+    std::lock_guard lock(mutex_);
+    for (auto &[root, folder] : root2folder) {
+      int i = 0;
+      for (const Project::Entry &entry : folder.entries) {
+        std::string reason;
+        if (match.IsMatch(entry.filename, &reason) &&
+            match_i.IsMatch(entry.filename, &reason)) {
+          bool interactive =
+              wfiles->GetFileByFilename(entry.filename) != nullptr;
+          pipeline::Index(
+              entry.filename, entry.args,
+              interactive ? IndexMode::Normal : IndexMode::NonInteractive, id);
+        } else {
+          LOG_V(1) << "[" << i << "/" << folder.entries.size() << "]: " << reason
+                   << "; skip " << entry.filename;
+        }
+        i++;
+      }
     }
   }
 
diff --git a/src/project.h b/src/project.h
index 59a89855..1cc5eff8 100644
--- a/src/project.h
+++ b/src/project.h
@@ -28,6 +28,7 @@ struct WorkingFiles;
 
 struct Project {
   struct Entry {
+    std::string root;
     std::string directory;
     std::string filename;
     std::vector<const char *> args;
@@ -36,14 +37,18 @@ struct Project {
     int id = -1;
   };
 
-  // Include directories for "" headers
-  std::vector<std::string> quote_include_directories;
-  // Include directories for <> headers
-  std::vector<std::string> angle_include_directories;
+  struct Folder {
+    std::string name;
+    // Include directories for <> headers
+    std::vector<std::string> angle_search_list;
+    // Include directories for "" headers
+    std::vector<std::string> quote_search_list;
+    std::vector<Entry> entries;
+    std::unordered_map<std::string, int> path2entry_index;
+  };
 
-  std::vector<Entry> entries;
   std::mutex mutex_;
-  std::unordered_map<std::string, int> path_to_entry_index;
+  std::unordered_map<std::string, Folder> root2folder;
 
   // Loads a project for the given |directory|.
   //