From ac7ce3aa6ad7ae6949247146e908cb38fc79925b Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 1 Dec 2024 16:35:30 +0800 Subject: [PATCH] Basic implementation of `CodeCompletion`. --- include/Feature/CodeCompletion.h | 62 ++++++++++++++++++++-------- include/Server/Scheduler.h | 7 +++- src/Feature/CodeCompletion.cpp | 70 +++++++++++++++++++++----------- src/Feature/SemanticTokens.cpp | 4 +- src/Server/Scheduler.cpp | 61 +++++++++++++++++++++++++--- src/Server/Server.cpp | 11 ++++- 6 files changed, 167 insertions(+), 48 deletions(-) diff --git a/include/Feature/CodeCompletion.h b/include/Feature/CodeCompletion.h index 803e2dfd..08aea680 100644 --- a/include/Feature/CodeCompletion.h +++ b/include/Feature/CodeCompletion.h @@ -1,11 +1,20 @@ #pragma once -#include +#include +#include + +namespace clice { +struct CompliationParams; +} namespace clice::proto { struct CompletionClientCapabilities {}; +struct CompletionOptions { + std::vector triggerCharacters; +}; + enum class CompletionItemKind { Text = 1, Method = 2, @@ -39,6 +48,17 @@ enum CompletionItemTag { Deprecated = 1, }; +struct InsertReplaceEdit { + /// The string to be inserted. + string newText; + + /// The range if the insert is requested. + Range insert; + + /// The range if the replace is requested. + Range replace; +}; + struct CompletionItem { /// The label of this completion item. /// If label details are provided the label itself should @@ -54,36 +74,46 @@ struct CompletionItem { CompletionItemKind kind; /// Tags for this completion item. - std::vector tags; + /// std::vector tags; - // FIXME: - // ... + /// A human-readable string with additional information + /// about this item, like type or symbol information. + /// string detail; + + /// A human-readable string that represents a doc-comment. + /// string documentation; + + /// A string that should be used when comparing this item + /// with other items. When omitted the label is used + /// as the sort text for this item. + /// string sortText; + + TextEdit textEdit; }; -struct CompletionParams {}; +using CompletionParams = TextDocumentPositionParams; + +using CompletionResult = std::vector; } // namespace clice::proto -namespace clice { -class Compiler; -} - namespace clice::config { -struct CodeCompletionOption { - // TODO: -}; +struct CodeCompletionOption {}; } // namespace clice::config namespace clice::feature { +json::Value capability(json::Value clientCapabilities); + /// Run code completion in given file and location. `compiler` should be /// set properly if any PCH or PCM is needed. Each completion requires a /// new compiler instance. -std::vector codeCompletion(Compiler& compiler, - llvm::StringRef filepath, - proto::Position position, - const config::CodeCompletionOption& option); +proto::CompletionResult codeCompletion(CompliationParams& compliation, + uint32_t line, + uint32_t column, + llvm::StringRef file, + const config::CodeCompletionOption& option); } // namespace clice::feature diff --git a/include/Server/Scheduler.h b/include/Server/Scheduler.h index ec81361a..bb7fc17a 100644 --- a/include/Server/Scheduler.h +++ b/include/Server/Scheduler.h @@ -5,6 +5,7 @@ #include "Async.h" #include "llvm/ADT/StringMap.h" #include "Compiler/Compiler.h" +#include "Feature/CodeCompletion.h" namespace clice { @@ -21,7 +22,7 @@ struct Task { struct File { bool isIdle = true; - + std::string content; /// The compiler instance of this file. ASTInfo compiler; @@ -40,6 +41,10 @@ public: async::promise buildAST(llvm::StringRef path, llvm::StringRef content); + async::promise codeComplete(llvm::StringRef path, + unsigned int line, + unsigned int column); + async::promise add(llvm::StringRef path, llvm::StringRef content); async::promise update(llvm::StringRef path, llvm::StringRef content); diff --git a/src/Feature/CodeCompletion.cpp b/src/Feature/CodeCompletion.cpp index 6d7b1f57..0248b7d6 100644 --- a/src/Feature/CodeCompletion.cpp +++ b/src/Feature/CodeCompletion.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,37 +9,42 @@ namespace { class CodeCompletionCollector final : public clang::CodeCompleteConsumer { public: - CodeCompletionCollector(clang::CodeCompleteOptions options) : - clang::CodeCompleteConsumer(options), allocator(new clang::GlobalCodeCompletionAllocator()), - info(allocator) { - // TODO: - } + CodeCompletionCollector(proto::CompletionResult& completions, uint32_t line, uint32_t column) : + clang::CodeCompleteConsumer({}), completions(completions), + allocator(new clang::GlobalCodeCompletionAllocator()), info(allocator), line(line), + column(column) {} void ProcessCodeCompleteResults(clang::Sema& sema, clang::CodeCompletionContext context, clang::CodeCompletionResult* results, unsigned count) final { - sema.getPreprocessor().getCodeCompletionLoc(); + auto loc = sema.getPreprocessor().getCodeCompletionLoc(); for(auto& result: llvm::make_range(results, results + count)) { - llvm::outs() << "Kind: " << result.Kind << " "; + auto& completion = completions.emplace_back(); switch(result.Kind) { case clang::CodeCompletionResult::RK_Declaration: { - result.getDeclaration()->dump(); + completion.label = result.Declaration->getDeclName().getAsString(); break; } case clang::CodeCompletionResult::RK_Keyword: { - llvm::outs() << result.Keyword << "\n"; + completion.label = result.Keyword; break; } case clang::CodeCompletionResult::RK_Macro: { - llvm::outs() << result.Macro << "\n"; + completion.label = result.Macro->getName(); break; } case clang::CodeCompletionResult::RK_Pattern: { - llvm::outs() << result.Pattern->getAsString() << "\n"; + completion.label = result.Pattern->getTypedText(); break; } } + completion.textEdit.newText = completion.label; + completion.textEdit.range = { + .start = {line - 1, column - 1 }, + .end = {line - 1, static_cast(column + completion.label.size()) - 1}, + }; + completion.kind = proto::CompletionItemKind::Text; } } @@ -53,22 +59,40 @@ public: private: std::shared_ptr allocator; clang::CodeCompletionTUInfo info; + proto::CompletionResult& completions; + uint32_t line; + uint32_t column; }; } // namespace -std::vector codeCompletion(CompliationParams& params, - llvm::StringRef filepath, - proto::Position position, - const config::CodeCompletionOption& option) { - // TODO: decode here. - auto result = codeCompleteAt(params, - position.line, - position.character, - filepath, - new CodeCompletionCollector({})); - if(result) {} - return {}; +json::Value capability(json::Value clientCapabilities) { + return json::Object{ + // We don't set `(` etc as allCommitCharacters as they interact + // poorly with snippet results. + // See https://github.com/clangd/vscode-clangd/issues/357 + // Hopefully we can use them one day without this side-effect: + // https://github.com/microsoft/vscode/issues/42544 + {"resolveProvider", false }, + // We do extra checks, e.g. that > is part of ->. + {"triggerCharacters", {".", "<", ">", ":", "\"", "/", "*"}}, + }; +} + +proto::CompletionResult codeCompletion(CompliationParams& compliation, + uint32_t line, + uint32_t column, + llvm::StringRef file, + const config::CodeCompletionOption& option) { + proto::CompletionResult completions; + auto consumer = new CodeCompletionCollector(completions, line, column); + + auto info = codeCompleteAt(compliation, line, column, file, consumer); + if(info) { + return completions; + } else { + std::terminate(); + } } } // namespace clice::feature diff --git a/src/Feature/SemanticTokens.cpp b/src/Feature/SemanticTokens.cpp index 0b5f1a8d..6f93d58e 100644 --- a/src/Feature/SemanticTokens.cpp +++ b/src/Feature/SemanticTokens.cpp @@ -710,8 +710,8 @@ public: } // namespace -proto::SemanticTokens semanticTokens(ASTInfo& compiler, llvm::StringRef filename) { - HighlightBuilder builder(compiler); +proto::SemanticTokens semanticTokens(ASTInfo& info, llvm::StringRef filename) { + HighlightBuilder builder(info); return builder.build(); } diff --git a/src/Server/Scheduler.cpp b/src/Server/Scheduler.cpp index 7d9225ad..80b65045 100644 --- a/src/Server/Scheduler.cpp +++ b/src/Server/Scheduler.cpp @@ -94,7 +94,7 @@ async::promise Scheduler::buildAST(llvm::StringRef filepath, llvm::StringR co_await (isModule ? updatePCM() : updatePCH(filepath, content, args)); Tracer tracer; - log::info("Start building AST for {0}", filepath.str()); + log::info("Start building AST for {0}", filepath); CompliationParams params; params.path = path; @@ -110,7 +110,7 @@ async::promise Scheduler::buildAST(llvm::StringRef filepath, llvm::StringR /// problem. auto info = clice::buildAST(params); if(!info) { - log::fatal("Failed to build AST for {0}", filepath.str()); + log::fatal("Failed to build AST for {0}", filepath); } return std::move(*info); }; @@ -118,11 +118,10 @@ async::promise Scheduler::buildAST(llvm::StringRef filepath, llvm::StringR auto compiler = co_await async::schedule_task(std::move(task)); auto& file = files[path]; + file.content = content; file.compiler = std::move(compiler); - log::info("Build AST successfully for {0}, elapsed {1}ms", - filepath.str(), - tracer.duration().count()); + log::info("Build AST successfully for {0}, elapsed {1}ms", filepath, tracer.duration()); if(!file.waitings.empty()) { auto task = std::move(file.waitings.front()); @@ -133,6 +132,58 @@ async::promise Scheduler::buildAST(llvm::StringRef filepath, llvm::StringR file.isIdle = true; } +async::promise Scheduler::codeComplete(llvm::StringRef filepath, + unsigned int line, + unsigned int column) { + auto iter = files.find(filepath); + if(iter == files.end()) { + log::fatal("File {0} is not building, skip code completion", filepath); + } + + if(iter->second.isIdle) { + /// If the file is already existed and is building, append the task to the waiting list. + co_await async::suspend([&](auto handle) { + iter->second.waitings.emplace_back(Task{ + .isBuild = true, + .waiting = handle, + }); + }); + } + + llvm::SmallString<128> path = filepath; + /// FIXME: lookup from CDB file and adjust and remove unnecessary arguments.1 + llvm::SmallVector args = { + "clang++", + "-std=c++20", + path.c_str(), + "-resource-dir", + "/home/ykiko/C++/clice2/build/lib/clang/20", + }; + + CompliationParams params; + params.path = path; + params.args = args; + params.content = iter->second.content; + + /// through arguments to judge is it a module. + bool isModule = false; + co_await (isModule ? updatePCM() : updatePCH(params.path, params.content, args)); + params.addPCH(pchs.at(filepath)); + + Tracer tracer; + log::info("Run code completion at {0}:{1}:{2}", filepath, line, column); + + auto task = [&] { + return feature::codeCompletion(params, line, column, filepath, {}); + }; + + auto result = co_await async::schedule_task(std::move(task)); + + log::info("Code completion for {0} is done, elapsed {1}ms", filepath, tracer.duration()); + + co_return result; +} + async::promise Scheduler::add(llvm::StringRef path, llvm::StringRef content) { co_await buildAST(path, content); co_return; diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index 93eb174b..6e82ac24 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -74,7 +74,11 @@ void Server::run(int argc, const char** argv) { async::promise Server::onInitialize(json::Value id, const proto::InitializeParams& params) { auto workplace = URI::resolve(params.workspaceFolders[0].uri); config::init(workplace); - async::write(std::move(id), json::serialize(proto::InitializeResult())); + auto result = json::serialize(proto::InitializeResult()); + auto capabilities = result.getAsObject()->get("capabilities")->getAsObject(); + /// FIXME: + capabilities->try_emplace("completionProvider", feature::capability(json::Value(nullptr))); + async::write(std::move(id), std::move(result)); co_return; } @@ -216,6 +220,11 @@ async::promise Server::onInlayHint(json::Value id, const proto::InlayHintP async::promise Server::onCodeCompletion(json::Value id, const proto::CompletionParams& params) { + auto path = URI::resolve(params.textDocument.uri); + auto result = co_await scheduler.codeComplete(path, + params.position.line + 1, + params.position.character + 1); + async::write(std::move(id), json::serialize(result)); co_return; }