Skip to content

Commit

Permalink
[clangd] Add code completion support
Browse files Browse the repository at this point in the history
Summary: Adds code completion support to clangd.

Reviewers: bkramer, malaperle-ericsson

Reviewed By: bkramer, malaperle-ericsson

Subscribers: stanionascu, malaperle-ericsson, cfe-commits

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

llvm-svn: 299421
  • Loading branch information
krasimirgg committed Apr 4, 2017
1 parent b7a90ef commit 6d2131a
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 46 deletions.
162 changes: 121 additions & 41 deletions clang-tools-extra/clangd/ASTManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,50 +83,54 @@ void ASTManager::runWorker() {
}

void ASTManager::parseFileAndPublishDiagnostics(StringRef File) {
auto &Unit = ASTs[File]; // Only one thread can access this at a time.

if (!Unit) {
Unit = createASTUnitForFile(File, this->Store);
} else {
// Do a reparse if this wasn't the first parse.
// FIXME: This might have the wrong working directory if it changed in the
// meantime.
Unit->Reparse(PCHs, getRemappedFiles(this->Store));
}
DiagnosticToReplacementMap LocalFixIts; // Temporary storage
std::string Diagnostics;
{
std::lock_guard<std::mutex> ASTGuard(ASTLock);
auto &Unit = ASTs[File]; // Only one thread can access this at a time.

if (!Unit)
return;
if (!Unit) {
Unit = createASTUnitForFile(File, this->Store);
} else {
// Do a reparse if this wasn't the first parse.
// FIXME: This might have the wrong working directory if it changed in the
// meantime.
Unit->Reparse(PCHs, getRemappedFiles(this->Store));
}

// Send the diagnotics to the editor.
// FIXME: If the diagnostic comes from a different file, do we want to
// show them all? Right now we drop everything not coming from the
// main file.
std::string Diagnostics;
DiagnosticToReplacementMap LocalFixIts; // Temporary storage
for (ASTUnit::stored_diag_iterator D = Unit->stored_diag_begin(),
DEnd = Unit->stored_diag_end();
D != DEnd; ++D) {
if (!D->getLocation().isValid() ||
!D->getLocation().getManager().isInMainFile(D->getLocation()))
continue;
Position P;
P.line = D->getLocation().getSpellingLineNumber() - 1;
P.character = D->getLocation().getSpellingColumnNumber();
Range R = {P, P};
Diagnostics +=
R"({"range":)" + Range::unparse(R) +
R"(,"severity":)" + std::to_string(getSeverity(D->getLevel())) +
R"(,"message":")" + llvm::yaml::escape(D->getMessage()) +
R"("},)";

// We convert to Replacements to become independent of the SourceManager.
clangd::Diagnostic Diag = {R, getSeverity(D->getLevel()), D->getMessage()};
auto &FixItsForDiagnostic = LocalFixIts[Diag];
for (const FixItHint &Fix : D->getFixIts()) {
FixItsForDiagnostic.push_back(clang::tooling::Replacement(
Unit->getSourceManager(), Fix.RemoveRange, Fix.CodeToInsert));
if (!Unit)
return;

// Send the diagnotics to the editor.
// FIXME: If the diagnostic comes from a different file, do we want to
// show them all? Right now we drop everything not coming from the
// main file.
for (ASTUnit::stored_diag_iterator D = Unit->stored_diag_begin(),
DEnd = Unit->stored_diag_end();
D != DEnd; ++D) {
if (!D->getLocation().isValid() ||
!D->getLocation().getManager().isInMainFile(D->getLocation()))
continue;
Position P;
P.line = D->getLocation().getSpellingLineNumber() - 1;
P.character = D->getLocation().getSpellingColumnNumber();
Range R = {P, P};
Diagnostics +=
R"({"range":)" + Range::unparse(R) +
R"(,"severity":)" + std::to_string(getSeverity(D->getLevel())) +
R"(,"message":")" + llvm::yaml::escape(D->getMessage()) +
R"("},)";

// We convert to Replacements to become independent of the SourceManager.
clangd::Diagnostic Diag = {R, getSeverity(D->getLevel()),
D->getMessage()};
auto &FixItsForDiagnostic = LocalFixIts[Diag];
for (const FixItHint &Fix : D->getFixIts()) {
FixItsForDiagnostic.push_back(clang::tooling::Replacement(
Unit->getSourceManager(), Fix.RemoveRange, Fix.CodeToInsert));
}
}
}
} // unlock ASTLock

// Put FixIts into place.
{
Expand Down Expand Up @@ -232,3 +236,79 @@ ASTManager::getFixIts(const clangd::Diagnostic &D) {
return I->second;
return {};
}

namespace {

class CompletionItemsCollector : public CodeCompleteConsumer {
std::vector<CompletionItem> *Items;
std::shared_ptr<clang::GlobalCodeCompletionAllocator> Allocator;
CodeCompletionTUInfo CCTUInfo;

public:
CompletionItemsCollector(std::vector<CompletionItem> *Items,
const CodeCompleteOptions &CodeCompleteOpts)
: CodeCompleteConsumer(CodeCompleteOpts, /*OutputIsBinary=*/false),
Items(Items),
Allocator(std::make_shared<clang::GlobalCodeCompletionAllocator>()),
CCTUInfo(Allocator) {}

void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context,
CodeCompletionResult *Results,
unsigned NumResults) override {
for (unsigned I = 0; I != NumResults; ++I) {
CodeCompletionString *CCS = Results[I].CreateCodeCompletionString(
S, Context, *Allocator, CCTUInfo,
CodeCompleteOpts.IncludeBriefComments);
if (CCS) {
CompletionItem Item;
assert(CCS->getTypedText());
Item.label = CCS->getTypedText();
if (CCS->getBriefComment())
Item.documentation = CCS->getBriefComment();
Items->push_back(std::move(Item));
}
}
}

GlobalCodeCompletionAllocator &getAllocator() override { return *Allocator; }

CodeCompletionTUInfo &getCodeCompletionTUInfo() override { return CCTUInfo; }
};

} // namespace

std::vector<CompletionItem>
ASTManager::codeComplete(StringRef Uri, unsigned Line, unsigned Column) {
CodeCompleteOptions CCO;
CCO.IncludeBriefComments = 1;
// This is where code completion stores dirty buffers. Need to free after
// completion.
SmallVector<const llvm::MemoryBuffer *, 4> OwnedBuffers;
SmallVector<StoredDiagnostic, 4> StoredDiagnostics;
IntrusiveRefCntPtr<DiagnosticsEngine> DiagEngine(
new DiagnosticsEngine(new DiagnosticIDs, new DiagnosticOptions));
std::vector<CompletionItem> Items;
CompletionItemsCollector Collector(&Items, CCO);
std::lock_guard<std::mutex> Guard(ASTLock);
auto &Unit = ASTs[Uri];
if (!Unit)
Unit = createASTUnitForFile(Uri, this->Store);
if (!Unit)
return {};
IntrusiveRefCntPtr<SourceManager> SourceMgr(
new SourceManager(*DiagEngine, Unit->getFileManager()));
StringRef File(Uri);
File.consume_front("file://");
// CodeComplete seems to require fresh LangOptions.
LangOptions LangOpts = Unit->getLangOpts();
// The language server protocol uses zero-based line and column numbers.
// The clang code completion uses one-based numbers.
Unit->CodeComplete(File, Line + 1, Column + 1, getRemappedFiles(this->Store),
CCO.IncludeMacros, CCO.IncludeCodePatterns,
CCO.IncludeBriefComments, Collector, PCHs, *DiagEngine,
LangOpts, *SourceMgr, Unit->getFileManager(),
StoredDiagnostics, OwnedBuffers);
for (const llvm::MemoryBuffer *Buffer : OwnedBuffers)
delete Buffer;
return Items;
}
20 changes: 18 additions & 2 deletions clang-tools-extra/clangd/ASTManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ class ASTManager : public DocumentStoreListener {

void onDocumentAdd(StringRef Uri) override;
// FIXME: Implement onDocumentRemove
// FIXME: Implement codeComplete

/// Get code completions at a specified \p Line and \p Column in \p File.
///
/// This function is thread-safe and returns completion items that own the
/// data they contain.
std::vector<CompletionItem> codeComplete(StringRef File, unsigned Line,
unsigned Column);

/// Get the fixes associated with a certain diagnostic as replacements.
///
Expand All @@ -59,7 +65,7 @@ class ASTManager : public DocumentStoreListener {
/// database is cached for subsequent accesses.
clang::tooling::CompilationDatabase *
getOrCreateCompilationDatabaseForFile(StringRef Uri);
// Craetes a new ASTUnit for the document at Uri.
// Creates a new ASTUnit for the document at Uri.
// FIXME: This calls chdir internally, which is thread unsafe.
std::unique_ptr<clang::ASTUnit>
createASTUnitForFile(StringRef Uri, const DocumentStore &Docs);
Expand All @@ -68,7 +74,17 @@ class ASTManager : public DocumentStoreListener {
void parseFileAndPublishDiagnostics(StringRef File);

/// Clang objects.

/// A map from Uri-s to ASTUnit-s. Guarded by \c ASTLock. ASTUnit-s are used
/// for generating diagnostics and fix-it-s asynchronously by the worker
/// thread and synchronously for code completion.
///
/// TODO(krasimir): code completion should always have priority over parsing
/// for diagnostics.
llvm::StringMap<std::unique_ptr<clang::ASTUnit>> ASTs;
/// A lock for access to the map \c ASTs.
std::mutex ASTLock;

llvm::StringMap<std::unique_ptr<clang::tooling::CompilationDatabase>>
CompilationDatabases;
std::shared_ptr<clang::PCHContainerOperations> PCHs;
Expand Down
2 changes: 2 additions & 0 deletions clang-tools-extra/clangd/ClangDMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ int main(int argc, char *argv[]) {
llvm::make_unique<TextDocumentFormattingHandler>(Out, Store));
Dispatcher.registerHandler("textDocument/codeAction",
llvm::make_unique<CodeActionHandler>(Out, AST));
Dispatcher.registerHandler("textDocument/completion",
llvm::make_unique<CompletionHandler>(Out, AST));

while (std::cin.good()) {
// A Language Server Protocol message starts with a HTTP header, delimited
Expand Down
72 changes: 72 additions & 0 deletions clang-tools-extra/clangd/Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,75 @@ CodeActionParams::parse(llvm::yaml::MappingNode *Params) {
}
return Result;
}

llvm::Optional<TextDocumentPositionParams>
TextDocumentPositionParams::parse(llvm::yaml::MappingNode *Params) {
TextDocumentPositionParams Result;
for (auto &NextKeyValue : *Params) {
auto *KeyString = dyn_cast<llvm::yaml::ScalarNode>(NextKeyValue.getKey());
if (!KeyString)
return llvm::None;

llvm::SmallString<10> KeyStorage;
StringRef KeyValue = KeyString->getValue(KeyStorage);
auto *Value =
dyn_cast_or_null<llvm::yaml::MappingNode>(NextKeyValue.getValue());
if (!Value)
return llvm::None;

llvm::SmallString<10> Storage;
if (KeyValue == "textDocument") {
auto Parsed = TextDocumentIdentifier::parse(Value);
if (!Parsed)
return llvm::None;
Result.textDocument = std::move(*Parsed);
} else if (KeyValue == "position") {
auto Parsed = Position::parse(Value);
if (!Parsed)
return llvm::None;
Result.position = std::move(*Parsed);
} else {
return llvm::None;
}
}
return Result;
}

std::string CompletionItem::unparse(const CompletionItem &CI) {
std::string Result = "{";
llvm::raw_string_ostream Os(Result);
assert(!CI.label.empty() && "completion item label is required");
Os << R"("label":")" << llvm::yaml::escape(CI.label) << R"(",)";
if (CI.kind != CompletionItemKind::Missing)
Os << R"("kind":)" << static_cast<int>(CI.kind) << R"(",)";
if (!CI.detail.empty())
Os << R"("detail":")" << llvm::yaml::escape(CI.detail) << R"(",)";
if (!CI.documentation.empty())
Os << R"("documentation":")" << llvm::yaml::escape(CI.documentation)
<< R"(",)";
if (!CI.sortText.empty())
Os << R"("sortText":")" << llvm::yaml::escape(CI.sortText) << R"(",)";
if (!CI.filterText.empty())
Os << R"("filterText":")" << llvm::yaml::escape(CI.filterText) << R"(",)";
if (!CI.insertText.empty())
Os << R"("insertText":")" << llvm::yaml::escape(CI.insertText) << R"(",)";
if (CI.insertTextFormat != InsertTextFormat::Missing) {
Os << R"("insertTextFormat":")" << static_cast<int>(CI.insertTextFormat)
<< R"(",)";
}
if (CI.textEdit)
Os << R"("textEdit":)" << TextEdit::unparse(*CI.textEdit) << ',';
if (!CI.additionalTextEdits.empty()) {
Os << R"("additionalTextEdits":[)";
for (const auto &Edit : CI.additionalTextEdits)
Os << TextEdit::unparse(Edit) << ",";
Os.flush();
// The list additionalTextEdits is guaranteed nonempty at this point.
// Replace the trailing comma with right brace.
Result.back() = ']';
}
Os.flush();
// Label is required, so Result is guaranteed to have a trailing comma.
Result.back() = '}';
return Result;
}
Loading

0 comments on commit 6d2131a

Please sign in to comment.