[clangd] Implementation of workspace/symbol request
authorMarc-Andre Laperle <marc-andre.laperle@ericsson.com>
Mon, 23 Apr 2018 20:00:52 +0000 (20:00 +0000)
committerMarc-Andre Laperle <marc-andre.laperle@ericsson.com>
Mon, 23 Apr 2018 20:00:52 +0000 (20:00 +0000)
Summary:
This is a basic implementation of the "workspace/symbol" request which is
used to find symbols by a string query. Since this is similar to code completion
in terms of result, this implementation reuses the "fuzzyFind" in order to get
matches. For now, the scoring algorithm is the same as code completion and
improvements could be done in the future.

The index model doesn't contain quite enough symbols for this to cover
common symbols like methods, enum class enumerators, functions in unamed
namespaces, etc. The index model will be augmented separately to achieve this.

Reviewers: sammccall, ilya-biryukov

Reviewed By: sammccall

Subscribers: jkorous, hokein, simark, sammccall, klimek, mgorny, ilya-biryukov, mgrang, jkorous-apple, ioeric, MaskRay, cfe-commits

Differential Revision: https://reviews.llvm.org/D44882

llvm-svn: 330637

22 files changed:
clang-tools-extra/clangd/CMakeLists.txt
clang-tools-extra/clangd/ClangdLSPServer.cpp
clang-tools-extra/clangd/ClangdLSPServer.h
clang-tools-extra/clangd/ClangdServer.cpp
clang-tools-extra/clangd/ClangdServer.h
clang-tools-extra/clangd/FindSymbols.cpp [new file with mode: 0644]
clang-tools-extra/clangd/FindSymbols.h [new file with mode: 0644]
clang-tools-extra/clangd/Protocol.cpp
clang-tools-extra/clangd/Protocol.h
clang-tools-extra/clangd/ProtocolHandlers.cpp
clang-tools-extra/clangd/ProtocolHandlers.h
clang-tools-extra/clangd/SourceCode.cpp
clang-tools-extra/clangd/SourceCode.h
clang-tools-extra/clangd/index/SymbolCollector.cpp
clang-tools-extra/clangd/tool/ClangdMain.cpp
clang-tools-extra/test/clangd/initialize-params-invalid.test
clang-tools-extra/test/clangd/initialize-params.test
clang-tools-extra/test/clangd/symbols.test [new file with mode: 0644]
clang-tools-extra/unittests/clangd/CMakeLists.txt
clang-tools-extra/unittests/clangd/FindSymbolsTests.cpp [new file with mode: 0644]
clang-tools-extra/unittests/clangd/SyncAPI.cpp
clang-tools-extra/unittests/clangd/SyncAPI.h

index 904fb3b..a5da0d2 100644 (file)
@@ -19,6 +19,7 @@ add_clang_library(clangDaemon
   Context.cpp
   Diagnostics.cpp
   DraftStore.cpp
+  FindSymbols.cpp
   FuzzyMatch.cpp
   GlobalCompilationDatabase.cpp
   Headers.cpp
index 8187f78..f8895b5 100644 (file)
@@ -86,6 +86,14 @@ std::vector<TextEdit> replacementsToEdits(StringRef Code,
   return Edits;
 }
 
+SymbolKindBitset defaultSymbolKinds() {
+  SymbolKindBitset Defaults;
+  for (size_t I = SymbolKindMin; I <= static_cast<size_t>(SymbolKind::Array);
+       ++I)
+    Defaults.set(I);
+  return Defaults;
+}
+
 } // namespace
 
 void ClangdLSPServer::onInitialize(InitializeParams &Params) {
@@ -97,6 +105,14 @@ void ClangdLSPServer::onInitialize(InitializeParams &Params) {
   CCOpts.EnableSnippets =
       Params.capabilities.textDocument.completion.completionItem.snippetSupport;
 
+  if (Params.capabilities.workspace && Params.capabilities.workspace->symbol &&
+      Params.capabilities.workspace->symbol->symbolKind) {
+    for (SymbolKind Kind :
+         *Params.capabilities.workspace->symbol->symbolKind->valueSet) {
+      SupportedSymbolKinds.set(static_cast<size_t>(Kind));
+    }
+  }
+
   reply(json::obj{
       {{"capabilities",
         json::obj{
@@ -122,6 +138,7 @@ void ClangdLSPServer::onInitialize(InitializeParams &Params) {
             {"documentHighlightProvider", true},
             {"hoverProvider", true},
             {"renameProvider", true},
+            {"workspaceSymbolProvider", true},
             {"executeCommandProvider",
              json::obj{
                  {"commands",
@@ -245,6 +262,20 @@ void ClangdLSPServer::onCommand(ExecuteCommandParams &Params) {
   }
 }
 
+void ClangdLSPServer::onWorkspaceSymbol(WorkspaceSymbolParams &Params) {
+  Server.workspaceSymbols(
+      Params.query, CCOpts.Limit,
+      [this](llvm::Expected<std::vector<SymbolInformation>> Items) {
+        if (!Items)
+          return replyError(ErrorCode::InternalError,
+                            llvm::toString(Items.takeError()));
+        for (auto &Sym : *Items)
+          Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds);
+
+        reply(json::ary(*Items));
+      });
+}
+
 void ClangdLSPServer::onRename(RenameParams &Params) {
   Path File = Params.textDocument.uri.file();
   llvm::Optional<std::string> Code = DraftMgr.getDraft(File);
@@ -422,6 +453,7 @@ ClangdLSPServer::ClangdLSPServer(JSONOutput &Out,
                                  llvm::Optional<Path> CompileCommandsDir,
                                  const ClangdServer::Options &Opts)
     : Out(Out), CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts),
+      SupportedSymbolKinds(defaultSymbolKinds()),
       Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {}
 
 bool ClangdLSPServer::run(std::istream &In, JSONStreamStyle InputStyle) {
index 82cf16f..f4884d3 100644 (file)
@@ -12,6 +12,7 @@
 
 #include "ClangdServer.h"
 #include "DraftStore.h"
+#include "FindSymbols.h"
 #include "GlobalCompilationDatabase.h"
 #include "Path.h"
 #include "Protocol.h"
@@ -69,6 +70,7 @@ private:
   void onDocumentHighlight(TextDocumentPositionParams &Params) override;
   void onFileEvent(DidChangeWatchedFilesParams &Params) override;
   void onCommand(ExecuteCommandParams &Params) override;
+  void onWorkspaceSymbol(WorkspaceSymbolParams &Params) override;
   void onRename(RenameParams &Parames) override;
   void onHover(TextDocumentPositionParams &Params) override;
   void onChangeConfiguration(DidChangeConfigurationParams &Params) override;
@@ -102,6 +104,8 @@ private:
   RealFileSystemProvider FSProvider;
   /// Options used for code completion
   clangd::CodeCompleteOptions CCOpts;
+  /// The supported kinds of the client.
+  SymbolKindBitset SupportedSymbolKinds;
 
   // Store of the current versions of the open documents.
   DraftStore DraftMgr;
index e73834b..e72cf40 100644 (file)
@@ -9,6 +9,7 @@
 
 #include "ClangdServer.h"
 #include "CodeComplete.h"
+#include "FindSymbols.h"
 #include "Headers.h"
 #include "SourceCode.h"
 #include "XRefs.h"
@@ -499,6 +500,11 @@ void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
   // invalidating other caches.
 }
 
+void ClangdServer::workspaceSymbols(
+    StringRef Query, int Limit, Callback<std::vector<SymbolInformation>> CB) {
+  CB(clangd::getWorkspaceSymbols(Query, Limit, Index));
+}
+
 std::vector<std::pair<Path, std::size_t>>
 ClangdServer::getUsedBytesPerFile() const {
   return WorkScheduler.getUsedBytesPerFile();
index 34af4b9..6e60705 100644 (file)
@@ -157,6 +157,10 @@ public:
   /// Get code hover for a given position.
   void findHover(PathRef File, Position Pos, Callback<Hover> CB);
 
+  /// Retrieve the top symbols from the workspace matching a query.
+  void workspaceSymbols(StringRef Query, int Limit,
+                        Callback<std::vector<SymbolInformation>> CB);
+
   /// Run formatting for \p Rng inside \p File with content \p Code.
   llvm::Expected<tooling::Replacements> formatRange(StringRef Code,
                                                     PathRef File, Range Rng);
diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
new file mode 100644 (file)
index 0000000..f2e7dd2
--- /dev/null
@@ -0,0 +1,141 @@
+//===--- FindSymbols.cpp ------------------------------------*- C++-*------===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "FindSymbols.h"
+
+#include "Logger.h"
+#include "SourceCode.h"
+#include "index/Index.h"
+#include "clang/Index/IndexSymbol.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/Path.h"
+
+namespace clang {
+namespace clangd {
+
+namespace {
+
+// Convert a index::SymbolKind to clangd::SymbolKind (LSP)
+// Note, some are not perfect matches and should be improved when this LSP
+// issue is addressed:
+// https://github.com/Microsoft/language-server-protocol/issues/344
+SymbolKind indexSymbolKindToSymbolKind(index::SymbolKind Kind) {
+  switch (Kind) {
+  case index::SymbolKind::Unknown:
+    return SymbolKind::Variable;
+  case index::SymbolKind::Module:
+    return SymbolKind::Module;
+  case index::SymbolKind::Namespace:
+    return SymbolKind::Namespace;
+  case index::SymbolKind::NamespaceAlias:
+    return SymbolKind::Namespace;
+  case index::SymbolKind::Macro:
+    return SymbolKind::String;
+  case index::SymbolKind::Enum:
+    return SymbolKind::Enum;
+  case index::SymbolKind::Struct:
+    return SymbolKind::Struct;
+  case index::SymbolKind::Class:
+    return SymbolKind::Class;
+  case index::SymbolKind::Protocol:
+    return SymbolKind::Interface;
+  case index::SymbolKind::Extension:
+    return SymbolKind::Interface;
+  case index::SymbolKind::Union:
+    return SymbolKind::Class;
+  case index::SymbolKind::TypeAlias:
+    return SymbolKind::Class;
+  case index::SymbolKind::Function:
+    return SymbolKind::Function;
+  case index::SymbolKind::Variable:
+    return SymbolKind::Variable;
+  case index::SymbolKind::Field:
+    return SymbolKind::Field;
+  case index::SymbolKind::EnumConstant:
+    return SymbolKind::EnumMember;
+  case index::SymbolKind::InstanceMethod:
+  case index::SymbolKind::ClassMethod:
+  case index::SymbolKind::StaticMethod:
+    return SymbolKind::Method;
+  case index::SymbolKind::InstanceProperty:
+  case index::SymbolKind::ClassProperty:
+  case index::SymbolKind::StaticProperty:
+    return SymbolKind::Property;
+  case index::SymbolKind::Constructor:
+  case index::SymbolKind::Destructor:
+    return SymbolKind::Method;
+  case index::SymbolKind::ConversionFunction:
+    return SymbolKind::Function;
+  case index::SymbolKind::Parameter:
+    return SymbolKind::Variable;
+  case index::SymbolKind::Using:
+    return SymbolKind::Namespace;
+  }
+  llvm_unreachable("invalid symbol kind");
+}
+
+} // namespace
+
+llvm::Expected<std::vector<SymbolInformation>>
+getWorkspaceSymbols(StringRef Query, int Limit,
+                    const SymbolIndex *const Index) {
+  std::vector<SymbolInformation> Result;
+  if (Query.empty() || !Index)
+    return Result;
+
+  auto Names = splitQualifiedName(Query);
+
+  FuzzyFindRequest Req;
+  Req.Query = Names.second;
+
+  // FuzzyFind doesn't want leading :: qualifier
+  bool IsGlobalQuery = Names.first.consume_front("::");
+  // Restrict results to the scope in the query string if present (global or
+  // not).
+  if (IsGlobalQuery || !Names.first.empty())
+    Req.Scopes = {Names.first};
+  if (Limit)
+    Req.MaxCandidateCount = Limit;
+  Index->fuzzyFind(Req, [&Result](const Symbol &Sym) {
+    // Prefer the definition over e.g. a function declaration in a header
+    auto &CD = Sym.Definition ? Sym.Definition : Sym.CanonicalDeclaration;
+    auto Uri = URI::parse(CD.FileURI);
+    if (!Uri) {
+      log(llvm::formatv(
+          "Workspace symbol: Could not parse URI '{0}' for symbol '{1}'.",
+          CD.FileURI, Sym.Name));
+      return;
+    }
+    // FIXME: Passing no HintPath here will work for "file" and "test" schemes
+    // because they don't use it but this might not work for other custom ones.
+    auto Path = URI::resolve(*Uri);
+    if (!Path) {
+      log(llvm::formatv("Workspace symbol: Could not resolve path for URI "
+                        "'{0}' for symbol '{1}'.",
+                        (*Uri).toString(), Sym.Name.str()));
+      return;
+    }
+    Location L;
+    L.uri = URIForFile((*Path));
+    Position Start, End;
+    Start.line = CD.Start.Line;
+    Start.character = CD.Start.Column;
+    End.line = CD.End.Line;
+    End.character = CD.End.Column;
+    L.range = {Start, End};
+    SymbolKind SK = indexSymbolKindToSymbolKind(Sym.SymInfo.Kind);
+    std::string Scope = Sym.Scope;
+    StringRef ScopeRef = Scope;
+    ScopeRef.consume_back("::");
+    Result.push_back({Sym.Name, SK, L, ScopeRef});
+  });
+  return Result;
+}
+
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h
new file mode 100644 (file)
index 0000000..1189175
--- /dev/null
@@ -0,0 +1,37 @@
+//===--- FindSymbols.h --------------------------------------*- C++-*------===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+//
+// Queries that provide a list of symbols matching a string.
+//
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H
+
+#include "Protocol.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace clangd {
+class SymbolIndex;
+
+/// Searches for the symbols matching \p Query. The syntax of \p Query can be
+/// the non-qualified name or fully qualified of a symbol. For example, "vector"
+/// will match the symbol std::vector and "std::vector" would also match it.
+/// Direct children of scopes (namepaces, etc) can be listed with a trailing
+/// "::". For example, "std::" will list all children of the std namespace and
+/// "::" alone will list all children of the global namespace.
+/// \p Limit limits the number of results returned (0 means no limit).
+llvm::Expected<std::vector<SymbolInformation>>
+getWorkspaceSymbols(llvm::StringRef Query, int Limit,
+                    const SymbolIndex *const Index);
+
+} // namespace clangd
+} // namespace clang
+
+#endif
index ec81a22..c7e4082 100644 (file)
@@ -176,6 +176,63 @@ bool fromJSON(const json::Expr &Params, CompletionClientCapabilities &R) {
   return true;
 }
 
+bool fromJSON(const json::Expr &E, SymbolKind &Out) {
+  if (auto T = E.asInteger()) {
+    if (*T < static_cast<int>(SymbolKind::File) ||
+        *T > static_cast<int>(SymbolKind::TypeParameter))
+      return false;
+    Out = static_cast<SymbolKind>(*T);
+    return true;
+  }
+  return false;
+}
+
+bool fromJSON(const json::Expr &E, std::vector<SymbolKind> &Out) {
+  if (auto *A = E.asArray()) {
+    Out.clear();
+    for (size_t I = 0; I < A->size(); ++I) {
+      SymbolKind KindOut;
+      if (fromJSON((*A)[I], KindOut))
+        Out.push_back(KindOut);
+    }
+    return true;
+  }
+  return false;
+}
+
+bool fromJSON(const json::Expr &Params, SymbolKindCapabilities &R) {
+  json::ObjectMapper O(Params);
+  return O && O.map("valueSet", R.valueSet);
+}
+
+SymbolKind adjustKindToCapability(SymbolKind Kind,
+                                  SymbolKindBitset &supportedSymbolKinds) {
+  auto KindVal = static_cast<size_t>(Kind);
+  if (KindVal >= SymbolKindMin && KindVal <= supportedSymbolKinds.size() &&
+      supportedSymbolKinds[KindVal])
+    return Kind;
+
+  switch (Kind) {
+  // Provide some fall backs for common kinds that are close enough.
+  case SymbolKind::Struct:
+    return SymbolKind::Class;
+  case SymbolKind::EnumMember:
+    return SymbolKind::Enum;
+  default:
+    return SymbolKind::String;
+  }
+}
+
+bool fromJSON(const json::Expr &Params, WorkspaceSymbolCapabilities &R) {
+  json::ObjectMapper O(Params);
+  return O && O.map("symbolKind", R.symbolKind);
+}
+
+bool fromJSON(const json::Expr &Params, WorkspaceClientCapabilities &R) {
+  json::ObjectMapper O(Params);
+  return O && O.map("symbol", R.symbol);
+}
+
 bool fromJSON(const json::Expr &Params, TextDocumentClientCapabilities &R) {
   json::ObjectMapper O(Params);
   if (!O)
@@ -189,6 +246,7 @@ bool fromJSON(const json::Expr &Params, ClientCapabilities &R) {
   if (!O)
     return false;
   O.map("textDocument", R.textDocument);
+  O.map("workspace", R.workspace);
   return true;
 }
 
@@ -351,6 +409,26 @@ bool fromJSON(const json::Expr &Params, ExecuteCommandParams &R) {
   return false; // Unrecognized command.
 }
 
+json::Expr toJSON(const SymbolInformation &P) {
+  return json::obj{
+      {"name", P.name},
+      {"kind", static_cast<int>(P.kind)},
+      {"location", P.location},
+      {"containerName", P.containerName},
+  };
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &O,
+                              const SymbolInformation &SI) {
+  O << SI.containerName << "::" << SI.name << " - " << toJSON(SI);
+  return O;
+}
+
+bool fromJSON(const json::Expr &Params, WorkspaceSymbolParams &R) {
+  json::ObjectMapper O(Params);
+  return O && O.map("query", R.query);
+}
+
 json::Expr toJSON(const Command &C) {
   auto Cmd = json::obj{{"title", C.title}, {"command", C.command}};
   if (C.workspaceEdit)
index fa57a0c..a54179c 100644 (file)
@@ -27,6 +27,7 @@
 #include "JSONExpr.h"
 #include "URI.h"
 #include "llvm/ADT/Optional.h"
+#include <bitset>
 #include <string>
 #include <vector>
 
@@ -237,6 +238,67 @@ struct CompletionClientCapabilities {
 };
 bool fromJSON(const json::Expr &, CompletionClientCapabilities &);
 
+/// A symbol kind.
+enum class SymbolKind {
+  File = 1,
+  Module = 2,
+  Namespace = 3,
+  Package = 4,
+  Class = 5,
+  Method = 6,
+  Property = 7,
+  Field = 8,
+  Constructor = 9,
+  Enum = 10,
+  Interface = 11,
+  Function = 12,
+  Variable = 13,
+  Constant = 14,
+  String = 15,
+  Number = 16,
+  Boolean = 17,
+  Array = 18,
+  Object = 19,
+  Key = 20,
+  Null = 21,
+  EnumMember = 22,
+  Struct = 23,
+  Event = 24,
+  Operator = 25,
+  TypeParameter = 26
+};
+
+constexpr auto SymbolKindMin = static_cast<size_t>(SymbolKind::File);
+constexpr auto SymbolKindMax = static_cast<size_t>(SymbolKind::TypeParameter);
+using SymbolKindBitset = std::bitset<SymbolKindMax + 1>;
+
+bool fromJSON(const json::Expr &, SymbolKind &);
+
+struct SymbolKindCapabilities {
+  /// The SymbolKinds that the client supports. If not set, the client only
+  /// supports <= SymbolKind::Array and will not fall back to a valid default
+  /// value.
+  llvm::Optional<std::vector<SymbolKind>> valueSet;
+};
+bool fromJSON(const json::Expr &, std::vector<SymbolKind> &);
+bool fromJSON(const json::Expr &, SymbolKindCapabilities &);
+SymbolKind adjustKindToCapability(SymbolKind Kind,
+                                  SymbolKindBitset &supportedSymbolKinds);
+
+struct WorkspaceSymbolCapabilities {
+  /// Capabilities SymbolKind.
+  llvm::Optional<SymbolKindCapabilities> symbolKind;
+};
+bool fromJSON(const json::Expr &, WorkspaceSymbolCapabilities &);
+
+// FIXME: most of the capabilities are missing from this struct. Only the ones
+// used by clangd are currently there.
+struct WorkspaceClientCapabilities {
+  /// Capabilities specific to `workspace/symbol`.
+  llvm::Optional<WorkspaceSymbolCapabilities> symbol;
+};
+bool fromJSON(const json::Expr &, WorkspaceClientCapabilities &);
+
 // FIXME: most of the capabilities are missing from this struct. Only the ones
 // used by clangd are currently there.
 struct TextDocumentClientCapabilities {
@@ -247,8 +309,7 @@ bool fromJSON(const json::Expr &, TextDocumentClientCapabilities &);
 
 struct ClientCapabilities {
   // Workspace specific client capabilities.
-  // NOTE: not used by clangd at the moment.
-  // WorkspaceClientCapabilities workspace;
+  llvm::Optional<WorkspaceClientCapabilities> workspace;
 
   // Text document specific client capabilities.
   TextDocumentClientCapabilities textDocument;
@@ -525,6 +586,31 @@ struct Command : public ExecuteCommandParams {
 
 json::Expr toJSON(const Command &C);
 
+/// Represents information about programming constructs like variables, classes,
+/// interfaces etc.
+struct SymbolInformation {
+  /// The name of this symbol.
+  std::string name;
+
+  /// The kind of this symbol.
+  SymbolKind kind;
+
+  /// The location of this symbol.
+  Location location;
+
+  /// The name of the symbol containing this symbol.
+  std::string containerName;
+};
+json::Expr toJSON(const SymbolInformation &);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &, const SymbolInformation &);
+
+/// The parameters of a Workspace Symbol Request.
+struct WorkspaceSymbolParams {
+  /// A non-empty query string
+  std::string query;
+};
+bool fromJSON(const json::Expr &, WorkspaceSymbolParams &);
+
 struct ApplyWorkspaceEditParams {
   WorkspaceEdit edit;
 };
index f5b4669..0432714 100644 (file)
@@ -72,4 +72,5 @@ void clangd::registerCallbackHandlers(JSONRPCDispatcher &Dispatcher,
            &ProtocolCallbacks::onDocumentHighlight);
   Register("workspace/didChangeConfiguration",
            &ProtocolCallbacks::onChangeConfiguration);
+  Register("workspace/symbol", &ProtocolCallbacks::onWorkspaceSymbol);
 }
index 6ed062c..dd18149 100644 (file)
@@ -49,6 +49,7 @@ public:
   virtual void onSwitchSourceHeader(TextDocumentIdentifier &Params) = 0;
   virtual void onFileEvent(DidChangeWatchedFilesParams &Params) = 0;
   virtual void onCommand(ExecuteCommandParams &Params) = 0;
+  virtual void onWorkspaceSymbol(WorkspaceSymbolParams &Params) = 0;
   virtual void onRename(RenameParams &Parames) = 0;
   virtual void onDocumentHighlight(TextDocumentPositionParams &Params) = 0;
   virtual void onHover(TextDocumentPositionParams &Params) = 0;
index 8e1db83..65a7ca9 100644 (file)
@@ -76,5 +76,13 @@ Range halfOpenToRange(const SourceManager &SM, CharSourceRange R) {
   return {Begin, End};
 }
 
+std::pair<llvm::StringRef, llvm::StringRef>
+splitQualifiedName(llvm::StringRef QName) {
+  size_t Pos = QName.rfind("::");
+  if (Pos == llvm::StringRef::npos)
+    return {StringRef(), QName};
+  return {QName.substr(0, Pos + 2), QName.substr(Pos + 2)};
+}
+
 } // namespace clangd
 } // namespace clang
index 6a2efd9..43fd070 100644 (file)
@@ -49,6 +49,11 @@ Position sourceLocToPosition(const SourceManager &SM, SourceLocation Loc);
 // Note that clang also uses closed source ranges, which this can't handle!
 Range halfOpenToRange(const SourceManager &SM, CharSourceRange R);
 
+/// From "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no
+/// qualifier.
+std::pair<llvm::StringRef, llvm::StringRef>
+splitQualifiedName(llvm::StringRef QName);
+
 } // namespace clangd
 } // namespace clang
 #endif
index c9d156e..cef30c7 100644 (file)
@@ -11,6 +11,7 @@
 #include "../AST.h"
 #include "../CodeCompletionStrings.h"
 #include "../Logger.h"
+#include "../SourceCode.h"
 #include "../URI.h"
 #include "CanonicalIncludes.h"
 #include "clang/AST/DeclCXX.h"
@@ -89,16 +90,6 @@ llvm::Optional<std::string> toURI(const SourceManager &SM, StringRef Path,
   return llvm::None;
 }
 
-// "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no qualifier.
-std::pair<llvm::StringRef, llvm::StringRef>
-splitQualifiedName(llvm::StringRef QName) {
-  assert(!QName.startswith("::") && "Qualified names should not start with ::");
-  size_t Pos = QName.rfind("::");
-  if (Pos == llvm::StringRef::npos)
-    return {StringRef(), QName};
-  return {QName.substr(0, Pos + 2), QName.substr(Pos + 2)};
-}
-
 bool shouldFilterDecl(const NamedDecl *ND, ASTContext *ASTCtx,
                       const SymbolCollector::Options &Opts) {
   using namespace clang::ast_matchers;
@@ -321,6 +312,7 @@ const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND,
   Policy.SuppressUnwrittenScope = true;
   ND.printQualifiedName(OS, Policy);
   OS.flush();
+  assert(!StringRef(QName).startswith("::"));
 
   Symbol S;
   S.ID = std::move(ID);
index 4d579b4..84d8a4e 100644 (file)
@@ -96,9 +96,9 @@ static llvm::cl::opt<PCHStorageFlag> PCHStorage(
         clEnumValN(PCHStorageFlag::Memory, "memory", "store PCHs in memory")),
     llvm::cl::init(PCHStorageFlag::Disk));
 
-static llvm::cl::opt<int> LimitCompletionResult(
-    "completion-limit",
-    llvm::cl::desc("Limit the number of completion results returned by clangd. "
+static llvm::cl::opt<int> LimitResults(
+    "limit-results",
+    llvm::cl::desc("Limit the number of results returned by clangd. "
                    "0 means no limit."),
     llvm::cl::init(100));
 
@@ -118,11 +118,11 @@ static llvm::cl::opt<Path> InputMirrorFile(
         "Mirror all LSP input to the specified file. Useful for debugging."),
     llvm::cl::init(""), llvm::cl::Hidden);
 
-static llvm::cl::opt<bool> EnableIndexBasedCompletion(
-    "enable-index-based-completion",
-    llvm::cl::desc(
-        "Enable index-based global code completion. "
-        "Clang uses an index built from symbols in opened files"),
+static llvm::cl::opt<bool> EnableIndex(
+    "index",
+    llvm::cl::desc("Enable index-based features such as global code completion "
+                   "and searching for symbols."
+                   "Clang uses an index built from symbols in opened files"),
     llvm::cl::init(true));
 
 static llvm::cl::opt<Path> YamlSymbolFile(
@@ -220,9 +220,9 @@ int main(int argc, char *argv[]) {
   }
   if (!ResourceDir.empty())
     Opts.ResourceDir = ResourceDir;
-  Opts.BuildDynamicSymbolIndex = EnableIndexBasedCompletion;
+  Opts.BuildDynamicSymbolIndex = EnableIndex;
   std::unique_ptr<SymbolIndex> StaticIdx;
-  if (EnableIndexBasedCompletion && !YamlSymbolFile.empty()) {
+  if (EnableIndex && !YamlSymbolFile.empty()) {
     StaticIdx = BuildStaticIndex(YamlSymbolFile);
     Opts.StaticIndex = StaticIdx.get();
   }
@@ -230,7 +230,7 @@ int main(int argc, char *argv[]) {
 
   clangd::CodeCompleteOptions CCOpts;
   CCOpts.IncludeIneligibleResults = IncludeIneligibleResults;
-  CCOpts.Limit = LimitCompletionResult;
+  CCOpts.Limit = LimitResults;
 
   // Initialize and run ClangdLSPServer.
   ClangdLSPServer LSPServer(Out, CCOpts, CompileCommandsDirPath, Opts);
index d98e110..928d9b6 100644 (file)
@@ -36,7 +36,8 @@
 # CHECK-NEXT:          ","
 # CHECK-NEXT:        ]
 # CHECK-NEXT:      },
-# CHECK-NEXT:      "textDocumentSync": 2
+# CHECK-NEXT:      "textDocumentSync": 2,
+# CHECK-NEXT:      "workspaceSymbolProvider": true
 # CHECK-NEXT:    }
 # CHECK-NEXT:  }
 ---
index d3bf136..a9ef4ba 100644 (file)
@@ -36,7 +36,8 @@
 # CHECK-NEXT:          ","
 # CHECK-NEXT:        ]
 # CHECK-NEXT:      },
-# CHECK-NEXT:      "textDocumentSync": 2
+# CHECK-NEXT:      "textDocumentSync": 2,
+# CHECK-NEXT:      "workspaceSymbolProvider": true
 # CHECK-NEXT:    }
 # CHECK-NEXT:  }
 ---
diff --git a/clang-tools-extra/test/clangd/symbols.test b/clang-tools-extra/test/clangd/symbols.test
new file mode 100644 (file)
index 0000000..adc73fb
--- /dev/null
@@ -0,0 +1,33 @@
+# RUN: clangd -lit-test < %s | FileCheck %s\r
+{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"workspace":{"symbol":{"symbolKind":{"valueSet": [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}}}},"trace":"off"}}\r
+---\r
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"#include <sstream>\nvoid foo(); int main() { foo(); }\n"}}}\r
+---\r
+{"jsonrpc":"2.0","id":1,"method":"workspace/symbol","params":{"query":"std::basic_ostringstream"}}\r
+#      CHECK:  "id": 1,\r
+# CHECK-NEXT:  "jsonrpc": "2.0",\r
+# CHECK-NEXT:    "result": [\r
+# CHECK-NEXT:      {\r
+# CHECK-NEXT:        "containerName": "std",\r
+# CHECK-NEXT:        "kind": 5,\r
+# CHECK-NEXT:        "location": {\r
+# CHECK-NEXT:          "range": {\r
+# CHECK-NEXT:            "end": {\r
+# CHECK-NEXT:              "character": {{.*}},\r
+# CHECK-NEXT:              "line": {{.*}}\r
+# CHECK-NEXT:            },\r
+# CHECK-NEXT:            "start": {\r
+# CHECK-NEXT:              "character": {{.*}},\r
+# CHECK-NEXT:              "line": {{.*}}\r
+# CHECK-NEXT:            }\r
+# CHECK-NEXT:          },\r
+# CHECK-NEXT:          "uri": "file://{{.*}}/sstream"\r
+# CHECK-NEXT:        },\r
+# CHECK-NEXT:        "name": "basic_ostringstream"\r
+# CHECK-NEXT:      }\r
+# CHECK-NEXT:    ]\r
+# CHECK-NEXT:}\r
+---\r
+{"jsonrpc":"2.0","id":3,"method":"shutdown"}\r
+---\r
+{"jsonrpc":"2.0","method":"exit"}\r
index f203509..a9aa968 100644 (file)
@@ -17,6 +17,7 @@ add_extra_unittest(ClangdTests
   ContextTests.cpp
   DraftStoreTests.cpp
   FileIndexTests.cpp
+  FindSymbolsTests.cpp
   FuzzyMatchTests.cpp
   GlobalCompilationDatabaseTests.cpp
   HeadersTests.cpp
diff --git a/clang-tools-extra/unittests/clangd/FindSymbolsTests.cpp b/clang-tools-extra/unittests/clangd/FindSymbolsTests.cpp
new file mode 100644 (file)
index 0000000..60fe052
--- /dev/null
@@ -0,0 +1,247 @@
+//===-- FindSymbolsTests.cpp -------------------------*- C++ -*------------===//
+//
+//                     The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+#include "Annotations.h"
+#include "ClangdServer.h"
+#include "FindSymbols.h"
+#include "SyncAPI.h"
+#include "TestFS.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+
+namespace {
+
+using ::testing::AllOf;
+using ::testing::AnyOf;
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::UnorderedElementsAre;
+
+class IgnoreDiagnostics : public DiagnosticsConsumer {
+  void onDiagnosticsReady(PathRef File,
+                          std::vector<Diag> Diagnostics) override {}
+};
+
+// GMock helpers for matching SymbolInfos items.
+MATCHER_P(Named, Name, "") { return arg.name == Name; }
+MATCHER_P(InContainer, ContainerName, "") {
+  return arg.containerName == ContainerName;
+}
+MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; }
+
+ClangdServer::Options optsForTests() {
+  auto ServerOpts = ClangdServer::optsForTest();
+  ServerOpts.BuildDynamicSymbolIndex = true;
+  return ServerOpts;
+}
+
+class WorkspaceSymbolsTest : public ::testing::Test {
+public:
+  WorkspaceSymbolsTest()
+      : Server(CDB, FSProvider, DiagConsumer, optsForTests()) {}
+
+protected:
+  MockFSProvider FSProvider;
+  MockCompilationDatabase CDB;
+  IgnoreDiagnostics DiagConsumer;
+  ClangdServer Server;
+  int Limit;
+
+  std::vector<SymbolInformation> getSymbols(StringRef Query) {
+    EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble";
+    auto SymbolInfos = runWorkspaceSymbols(Server, Query, Limit);
+    EXPECT_TRUE(bool(SymbolInfos)) << "workspaceSymbols returned an error";
+    return *SymbolInfos;
+  }
+
+  void addFile(StringRef FileName, StringRef Contents) {
+    auto Path = testPath(FileName);
+    FSProvider.Files[Path] = Contents;
+    Server.addDocument(Path, Contents);
+  }
+};
+
+} // namespace
+
+TEST_F(WorkspaceSymbolsTest, NoMacro) {
+  addFile("foo.cpp", R"cpp(
+      #define MACRO X
+      )cpp");
+
+  // Macros are not in the index.
+  EXPECT_THAT(getSymbols("macro"), IsEmpty());
+}
+
+TEST_F(WorkspaceSymbolsTest, NoLocals) {
+  addFile("foo.cpp", R"cpp(
+      void test(int FirstParam, int SecondParam) {
+        struct LocalClass {};
+        int local_var;
+      })cpp");
+  EXPECT_THAT(getSymbols("l"), IsEmpty());
+  EXPECT_THAT(getSymbols("p"), IsEmpty());
+}
+
+TEST_F(WorkspaceSymbolsTest, Globals) {
+  addFile("foo.h", R"cpp(
+      int global_var;
+
+      int global_func();
+
+      struct GlobalStruct {};)cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      )cpp");
+  EXPECT_THAT(getSymbols("global"),
+              UnorderedElementsAre(AllOf(Named("GlobalStruct"), InContainer(""),
+                                         WithKind(SymbolKind::Struct)),
+                                   AllOf(Named("global_func"), InContainer(""),
+                                         WithKind(SymbolKind::Function)),
+                                   AllOf(Named("global_var"), InContainer(""),
+                                         WithKind(SymbolKind::Variable))));
+}
+
+TEST_F(WorkspaceSymbolsTest, Unnamed) {
+  addFile("foo.h", R"cpp(
+      struct {
+        int InUnnamed;
+      } UnnamedStruct;)cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      )cpp");
+  EXPECT_THAT(getSymbols("UnnamedStruct"),
+              ElementsAre(AllOf(Named("UnnamedStruct"),
+                                WithKind(SymbolKind::Variable))));
+  EXPECT_THAT(getSymbols("InUnnamed"), IsEmpty());
+}
+
+TEST_F(WorkspaceSymbolsTest, InMainFile) {
+  addFile("foo.cpp", R"cpp(
+      int test() {
+      }
+      )cpp");
+  EXPECT_THAT(getSymbols("test"), IsEmpty());
+}
+
+TEST_F(WorkspaceSymbolsTest, Namespaces) {
+  addFile("foo.h", R"cpp(
+      namespace ans1 {
+        int ai1;
+      namespace ans2 {
+        int ai2;
+      }
+      }
+      )cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      )cpp");
+  EXPECT_THAT(
+      getSymbols("a"),
+      UnorderedElementsAre(AllOf(Named("ans1"), InContainer("")),
+                           AllOf(Named("ai1"), InContainer("ans1")),
+                           AllOf(Named("ans2"), InContainer("ans1")),
+                           AllOf(Named("ai2"), InContainer("ans1::ans2"))));
+  EXPECT_THAT(getSymbols("::"),
+              ElementsAre(AllOf(Named("ans1"), InContainer(""))));
+  EXPECT_THAT(getSymbols("::a"),
+              ElementsAre(AllOf(Named("ans1"), InContainer(""))));
+  EXPECT_THAT(getSymbols("ans1::"),
+              UnorderedElementsAre(AllOf(Named("ai1"), InContainer("ans1")),
+                                   AllOf(Named("ans2"), InContainer("ans1"))));
+  EXPECT_THAT(getSymbols("::ans1"),
+              ElementsAre(AllOf(Named("ans1"), InContainer(""))));
+  EXPECT_THAT(getSymbols("::ans1::"),
+              UnorderedElementsAre(AllOf(Named("ai1"), InContainer("ans1")),
+                                   AllOf(Named("ans2"), InContainer("ans1"))));
+  EXPECT_THAT(getSymbols("::ans1::ans2"),
+              ElementsAre(AllOf(Named("ans2"), InContainer("ans1"))));
+  EXPECT_THAT(getSymbols("::ans1::ans2::"),
+              ElementsAre(AllOf(Named("ai2"), InContainer("ans1::ans2"))));
+}
+
+TEST_F(WorkspaceSymbolsTest, AnonymousNamespace) {
+  addFile("foo.h", R"cpp(
+      namespace {
+      void test() {}
+      }
+      )cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      )cpp");
+  EXPECT_THAT(getSymbols("test"), IsEmpty());
+}
+
+TEST_F(WorkspaceSymbolsTest, MultiFile) {
+  addFile("foo.h", R"cpp(
+      int foo() {
+      }
+      )cpp");
+  addFile("foo2.h", R"cpp(
+      int foo2() {
+      }
+      )cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      #include "foo2.h"
+      )cpp");
+  EXPECT_THAT(getSymbols("foo"),
+              UnorderedElementsAre(AllOf(Named("foo"), InContainer("")),
+                                   AllOf(Named("foo2"), InContainer(""))));
+}
+
+TEST_F(WorkspaceSymbolsTest, GlobalNamespaceQueries) {
+  addFile("foo.h", R"cpp(
+      int foo() {
+      }
+      class Foo {
+        int a;
+      };
+      namespace ns {
+      int foo2() {
+      }
+      }
+      )cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      )cpp");
+  EXPECT_THAT(
+      getSymbols("::"),
+      UnorderedElementsAre(
+          AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Class)),
+          AllOf(Named("foo"), InContainer(""), WithKind(SymbolKind::Function)),
+          AllOf(Named("ns"), InContainer(""),
+                WithKind(SymbolKind::Namespace))));
+  EXPECT_THAT(getSymbols(":"), IsEmpty());
+  EXPECT_THAT(getSymbols(""), IsEmpty());
+}
+
+TEST_F(WorkspaceSymbolsTest, WithLimit) {
+  addFile("foo.h", R"cpp(
+      int foo;
+      int foo2;
+      )cpp");
+  addFile("foo.cpp", R"cpp(
+      #include "foo.h"
+      )cpp");
+  EXPECT_THAT(getSymbols("foo"),
+              ElementsAre(AllOf(Named("foo"), InContainer(""),
+                                WithKind(SymbolKind::Variable)),
+                          AllOf(Named("foo2"), InContainer(""),
+                                WithKind(SymbolKind::Variable))));
+
+  Limit = 1;
+  EXPECT_THAT(getSymbols("foo"),
+              ElementsAre(AnyOf((Named("foo"), InContainer("")),
+                                AllOf(Named("foo2"), InContainer("")))));
+}
+
+} // namespace clangd
+} // namespace clang
index 28393ee..0a1c598 100644 (file)
@@ -110,5 +110,12 @@ std::string runDumpAST(ClangdServer &Server, PathRef File) {
   return std::move(*Result);
 }
 
+llvm::Expected<std::vector<SymbolInformation>>
+runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit) {
+  llvm::Optional<llvm::Expected<std::vector<SymbolInformation>>> Result;
+  Server.workspaceSymbols(Query, Limit, capture(Result));
+  return std::move(*Result);
+}
+
 } // namespace clangd
 } // namespace clang
index c0fcb93..d4d2ac8 100644 (file)
@@ -41,6 +41,9 @@ runRename(ClangdServer &Server, PathRef File, Position Pos, StringRef NewName);
 
 std::string runDumpAST(ClangdServer &Server, PathRef File);
 
+llvm::Expected<std::vector<SymbolInformation>>
+runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit);
+
 } // namespace clangd
 } // namespace clang