diff --git a/include/Compiler/Compilation.h b/include/Compiler/Compilation.h index a6ed7789..e115a523 100644 --- a/include/Compiler/Compilation.h +++ b/include/Compiler/Compilation.h @@ -44,7 +44,7 @@ struct CompilationParams { void add_remapped_file(llvm::StringRef path, llvm::StringRef content, std::uint32_t bound = -1) { - if(bound != 0 && bound != -1) { + if(bound != -1) { assert(bound <= content.size()); content = content.substr(0, bound); } diff --git a/include/Feature/DocumentLink.h b/include/Feature/DocumentLink.h index 90144af1..c6942eb2 100644 --- a/include/Feature/DocumentLink.h +++ b/include/Feature/DocumentLink.h @@ -18,10 +18,10 @@ struct DocumentLink { using DocumentLinks = std::vector; /// Generate document link for main file. -DocumentLinks documentLinks(CompilationUnit& unit); +DocumentLinks document_links(CompilationUnit& unit); /// Generate document link for all source file. -index::Shared indexDocumentLink(CompilationUnit& unit); +index::Shared index_document_link(CompilationUnit& unit); } // namespace clice::feature diff --git a/include/Protocol/Feature/DocumentLink.h b/include/Protocol/Feature/DocumentLink.h index d781ea80..d9ab2534 100644 --- a/include/Protocol/Feature/DocumentLink.h +++ b/include/Protocol/Feature/DocumentLink.h @@ -4,8 +4,37 @@ namespace clice::proto { -struct DocumentLinkClientCapabilities {}; +struct DocumentLinkClientCapabilities { + /// Whether the client supports the `tooltip` property on `DocumentLink`. + bool tooltipSupport = false; +}; -struct DocumentLinkOptions {}; +struct DocumentLinkOptions { + /// Document links have a resolve provider as well. + bool resolveProvider; +}; + +struct DocumentLinkParams { + /// The document to provide document links for. + TextDocumentIdentifier textDocument; +}; + +/// A document link is a range in a text document that links to an internal or +/// external resource, like another text document or a web site. +struct DocumentLink { + /// The range this link applies to. + Range range; + + /// The uri this link points to. If missing a resolve request is sent later. + URI target; + + /// The tooltip text when you hover over this link. + /// + /// If a tooltip is provided, is will be displayed in a string that includes + /// instructions on how to trigger the link, such as `{0} (ctrl + click)`. + /// The specific instructions vary depending on OS, user settings, and + /// localization. + /// FIXME: string tooltip; +}; } // namespace clice::proto diff --git a/include/Protocol/Initialize.h b/include/Protocol/Initialize.h index 2e5488ad..b8b2e281 100644 --- a/include/Protocol/Initialize.h +++ b/include/Protocol/Initialize.h @@ -117,7 +117,7 @@ struct ServerCapabilities { /// FIXME: CodeLensOptions codeLensProvider; /// The server provides document link support. - /// FIXME: DocumentLinkOptions documentLinkProvider; + DocumentLinkOptions documentLinkProvider; /// The server provides color provider support. /// FIXME: DocumentColorOptions colorProvider; diff --git a/include/Server/Server.h b/include/Server/Server.h index 91c64535..614bd7aa 100644 --- a/include/Server/Server.h +++ b/include/Server/Server.h @@ -7,6 +7,7 @@ #include "Compiler/Command.h" #include "Compiler/Preamble.h" #include "Compiler/Diagnostic.h" +#include "Feature/DocumentLink.h" #include "Protocol/Protocol.h" namespace clice { @@ -22,6 +23,7 @@ struct OpenFile { std::optional pch; async::Task pch_build_task; async::Event pch_built_event; + std::vector pch_includes; /// For each opened file, we would like to build an AST for it. std::shared_ptr ast; @@ -103,11 +105,13 @@ private: async::Task<> on_did_close(proto::DidCloseTextDocumentParams params); private: + async::Task on_completion(proto::CompletionParams params); + async::Task on_hover(proto::HoverParams params); - async::Task on_semantic_token(proto::SemanticTokensParams params); + async::Task on_document_link(proto::DocumentLinkParams params); - async::Task on_completion(proto::CompletionParams params); + async::Task on_semantic_token(proto::SemanticTokensParams params); private: /// The current request id. diff --git a/include/Test/Tester.h b/include/Test/Tester.h index 40703543..0c4875a1 100644 --- a/include/Test/Tester.h +++ b/include/Test/Tester.h @@ -57,6 +57,7 @@ struct Tester { } bool compile_with_pch(llvm::StringRef standard = "-std=c++20") { + params.diagnostics = std::make_shared>(); auto command = std::format("clang++ {} {} -fms-extensions", standard, src_path); database.update_command("fake", src_path, command); @@ -73,9 +74,11 @@ struct Tester { for(auto& [file, source]: sources.all_files) { if(file == src_path) { auto bound = compute_preamble_bound(source.content); - params.add_remapped_file(file, source.content.substr(0, bound)); + params.add_remapped_file(file, source.content, bound); } else { - params.add_remapped_file(file, source.content); + /// FIXME: This is a workaround. + std::string path = path::is_absolute(file) ? file.str() : path::join(".", file); + params.add_remapped_file(path, source.content); } } @@ -84,6 +87,9 @@ struct Tester { auto unit = clice::compile(params, info); if(!unit) { llvm::outs() << unit.error() << "\n"; + for(auto& diag: *params.diagnostics) { + clice::println("{}", diag.message); + } return false; } } @@ -92,7 +98,13 @@ struct Tester { params.output_file.clear(); params.pch = {info.path, info.preamble.size()}; for(auto& [file, source]: sources.all_files) { - params.add_remapped_file(file, source.content); + if(file == src_path) { + params.add_remapped_file(file, source.content); + } else { + /// FIXME: This is a workaround. + std::string path = path::is_absolute(file) ? file.str() : path::join(".", file); + params.add_remapped_file(path, source.content); + } } auto unit = clice::compile(params); diff --git a/src/Compiler/Compilation.cpp b/src/Compiler/Compilation.cpp index 266d8430..1b011f3d 100644 --- a/src/Compiler/Compilation.cpp +++ b/src/Compiler/Compilation.cpp @@ -152,8 +152,8 @@ template CompilationResult run_clang(CompilationParams& params, const auto& before_execute, const auto& after_execute) { - auto diagnostics = params.diagnostics ? std::move(params.diagnostics) - : std::make_shared>(); + auto diagnostics = + params.diagnostics ? params.diagnostics : std::make_shared>(); auto diagnostic_engine = clang::CompilerInstance::createDiagnostics(*params.vfs, new clang::DiagnosticOptions(), @@ -188,10 +188,10 @@ CompilationResult run_clang(CompilationParams& params, } auto& pp = instance->getPreprocessor(); - // FIXME: clang-tidy, include-fixer, etc? + /// FIXME: clang-tidy, include-fixer, etc? - // `BeginSourceFile` may create new preprocessor, so all operations related to preprocessor - // should be done after `BeginSourceFile`. + /// `BeginSourceFile` may create new preprocessor, so all operations related to preprocessor + /// should be done after `BeginSourceFile`. /// Collect directives. llvm::DenseMap directives; diff --git a/src/Feature/DocumentLink.cpp b/src/Feature/DocumentLink.cpp index 29080d00..76e792c1 100644 --- a/src/Feature/DocumentLink.cpp +++ b/src/Feature/DocumentLink.cpp @@ -7,9 +7,38 @@ namespace clice::feature { namespace {} -DocumentLinks documentLinks(CompilationUnit& unit); +DocumentLinks document_links(CompilationUnit& unit) { + DocumentLinks links; -index::Shared indexDocumentLink(CompilationUnit& unit) { + auto& interested_diretive = unit.directives()[unit.interested_file()]; + for(auto include: interested_diretive.includes) { + auto [_, range] = unit.decompose_range(include.filename_range); + links.emplace_back(range, unit.file_path(include.fid).str()); + } + + auto content = unit.interested_content(); + for(auto& has_include: interested_diretive.has_includes) { + /// If the include path is empty, skip it. + if(has_include.fid.isInvalid()) { + continue; + } + + auto location = has_include.location; + auto [_, offset] = unit.decompose_location(location); + + /// FIXME: handle incomplete code, the <> or "" may not be in pair. + auto sub_content = content.substr(offset); + char c = sub_content[0] == '<' ? '>' : '"'; + std::uint32_t end_offset = offset + sub_content.find_first_of(c, 1) + 1; + + links.emplace_back(LocalSourceRange{offset, end_offset}, + unit.file_path(has_include.fid).str()); + } + + return links; +} + +index::Shared index_document_link(CompilationUnit& unit) { index::Shared result; for(auto& [fid, diretives]: unit.directives()) { diff --git a/src/Index/FeatureIndex.cpp b/src/Index/FeatureIndex.cpp index 383a8917..1a033025 100644 --- a/src/Index/FeatureIndex.cpp +++ b/src/Index/FeatureIndex.cpp @@ -69,7 +69,7 @@ Shared> FeatureIndex::build(CompilationUnit& unit) { indices[fid].foldings = std::move(result); } - for(auto&& [fid, result]: feature::indexDocumentLink(unit)) { + for(auto&& [fid, result]: feature::index_document_link(unit)) { indices[fid].links = std::move(result); } diff --git a/src/Server/Document.cpp b/src/Server/Document.cpp index 6c04242c..0fb877ac 100644 --- a/src/Server/Document.cpp +++ b/src/Server/Document.cpp @@ -73,6 +73,8 @@ async::Task<> Server::build_pch(std::string path, std::string content) { log::info("Start building PCH for {}, command: [{}]", path, command); + std::vector links; + /// PCH file is written until destructing, Add a single block /// for it. bool cond = co_await async::submit([&] { @@ -86,6 +88,8 @@ async::Task<> Server::build_pch(std::string path, std::string content) { return false; } + links = feature::document_links(*result); + /// TODO: index PCH. return true; @@ -98,6 +102,8 @@ async::Task<> Server::build_pch(std::string path, std::string content) { auto& openFile = server.opening_files[path]; /// Update the built PCH info. openFile.pch = std::move(info); + openFile.pch_includes = std::move(links); + /// Resume waiters on this event. openFile.pch_built_event.set(); openFile.pch_built_event.clear(); diff --git a/src/Server/Feature.cpp b/src/Server/Feature.cpp index b6b5891c..f27f6c8f 100644 --- a/src/Server/Feature.cpp +++ b/src/Server/Feature.cpp @@ -1,55 +1,13 @@ #include "Server/Server.h" #include "Server/Convert.h" #include "Compiler/Compilation.h" +#include "Feature/CodeCompletion.h" #include "Feature/Hover.h" +#include "Feature/DocumentLink.h" +#include "Feature/SemanticToken.h" namespace clice { -async::Task Server::on_hover(proto::HoverParams params) { - auto path = mapping.to_path(params.textDocument.uri); - - auto opening_file = &opening_files[path]; - auto guard = co_await opening_file->ast_built_lock.try_lock(); - - auto offset = to_offset(kind, opening_file->content, params.position); - - opening_file = &opening_files[path]; - auto content = opening_file->content; - auto ast = opening_file->ast; - if(!ast) { - co_return json::Value(nullptr); - } - - co_return co_await async::submit([kind = this->kind, offset, &ast] { - auto hover = feature::hover(*ast, offset); - - proto::Hover result; - result.contents.kind = "markdown"; - result.contents.value = std::format("{}: {}", hover.kind.name(), hover.name); - - return json::serialize(result); - }); -} - -async::Task Server::on_semantic_token(proto::SemanticTokensParams params) { - auto path = mapping.to_path(params.textDocument.uri); - - auto opening_file = &opening_files[path]; - auto guard = co_await opening_file->ast_built_lock.try_lock(); - - opening_file = &opening_files[path]; - auto content = opening_file->content; - auto ast = opening_file->ast; - if(!ast) { - co_return json::Value(nullptr); - } - - co_return co_await async::submit([kind = this->kind, &ast] { - auto tokens = feature::semantic_tokens(*ast); - return proto::to_json(kind, ast->interested_content(), tokens); - }); -} - async::Task Server::on_completion(proto::CompletionParams params) { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = &opening_files[path]; @@ -78,4 +36,80 @@ async::Task Server::on_completion(proto::CompletionParams params) { } } +async::Task Server::on_hover(proto::HoverParams params) { + auto path = mapping.to_path(params.textDocument.uri); + + auto opening_file = &opening_files[path]; + auto guard = co_await opening_file->ast_built_lock.try_lock(); + + auto offset = to_offset(kind, opening_file->content, params.position); + + opening_file = &opening_files[path]; + auto content = opening_file->content; + auto ast = opening_file->ast; + if(!ast) { + co_return json::Value(nullptr); + } + + co_return co_await async::submit([kind = this->kind, offset, &ast] { + auto hover = feature::hover(*ast, offset); + + proto::Hover result; + result.contents.kind = "markdown"; + result.contents.value = std::format("{}: {}", hover.kind.name(), hover.name); + + return json::serialize(result); + }); +} + +async::Task Server::on_document_link(proto::DocumentLinkParams params) { + auto path = mapping.to_path(params.textDocument.uri); + + auto opening_file = &opening_files[path]; + auto guard = co_await opening_file->ast_built_lock.try_lock(); + + opening_file = &opening_files[path]; + auto content = opening_file->content; + auto ast = opening_file->ast; + if(!ast) { + co_return json::Value(nullptr); + } + + auto pch_links = opening_file->pch_includes; + auto mapping = this->mapping; + + co_return co_await async::submit([&, kind = this->kind] { + auto links = feature::document_links(*ast); + links.insert(links.begin(), pch_links.begin(), pch_links.end()); + + PositionConverter converter(content, kind); + converter.to_positions(links, [](feature::DocumentLink& link) { return link.range; }); + + std::vector result; + for(auto& link: links) { + result.emplace_back(converter.lookup(link.range), mapping.to_uri(link.file)); + } + return json::serialize(result); + }); +} + +async::Task Server::on_semantic_token(proto::SemanticTokensParams params) { + auto path = mapping.to_path(params.textDocument.uri); + + auto opening_file = &opening_files[path]; + auto guard = co_await opening_file->ast_built_lock.try_lock(); + + opening_file = &opening_files[path]; + auto content = opening_file->content; + auto ast = opening_file->ast; + if(!ast) { + co_return json::Value(nullptr); + } + + co_return co_await async::submit([kind = this->kind, &ast] { + auto tokens = feature::semantic_tokens(*ast); + return proto::to_json(kind, ast->interested_content(), tokens); + }); +} + } // namespace clice diff --git a/src/Server/Lifecycle.cpp b/src/Server/Lifecycle.cpp index 513571ee..b95764d0 100644 --- a/src/Server/Lifecycle.cpp +++ b/src/Server/Lifecycle.cpp @@ -39,14 +39,17 @@ async::Task Server::on_initialize(proto::InitializeParams params) { capabilities.textDocumentSync.change = proto::TextDocumentSyncKind::Full; capabilities.textDocumentSync.save = true; - /// Hover - capabilities.hoverProvider = true; - /// Completion capabilities.completionProvider.triggerCharacters = {".", "<", ">", ":", "\"", "/", "*"}; capabilities.completionProvider.resolveProvider = false; capabilities.completionProvider.completionItem.labelDetailsSupport = true; + /// Hover + capabilities.hoverProvider = true; + + /// DocumentLink + capabilities.documentLinkProvider.resolveProvider = false; + /// Semantic tokens. capabilities.semanticTokensProvider.range = false; capabilities.semanticTokensProvider.full = true; diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index 8e6575ac..c7f4c4ba 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -63,8 +63,9 @@ Server::Server() { register_callback<&Server::on_did_save>("textDocument/didSave"); register_callback<&Server::on_did_close>("textDocument/didClose"); - register_callback<&Server::on_hover>("textDocument/hover"); register_callback<&Server::on_completion>("textDocument/completion"); + register_callback<&Server::on_hover>("textDocument/hover"); + register_callback<&Server::on_document_link>("textDocument/documentLink"); register_callback<&Server::on_semantic_token>("textDocument/semanticTokens/full"); } diff --git a/tests/unit/Feature/DocumentLink.cpp b/tests/unit/Feature/DocumentLink.cpp index a314c6b5..2e40eb0a 100644 --- a/tests/unit/Feature/DocumentLink.cpp +++ b/tests/unit/Feature/DocumentLink.cpp @@ -6,13 +6,13 @@ namespace clice::testing { namespace { struct DocumentLink : TestFixture { - index::Shared result; + std::vector links; using Self = DocumentLink; void run() { Tester::compile(); - result = feature::indexDocumentLink(*unit); + links = feature::document_links(*unit); } void EXPECT_LINK(this Self& self, @@ -20,7 +20,7 @@ struct DocumentLink : TestFixture { llvm::StringRef name, llvm::StringRef path, LocationChain chain = LocationChain()) { - auto& link = self.result[self.unit->interested_file()][index]; + auto& link = self.links[index]; EXPECT_EQ(link.range, self.range(name, "main.cpp"), chain); @@ -31,7 +31,7 @@ struct DocumentLink : TestFixture { } void dump() { - clice::println("{}", clice::dump(result[unit->interested_file()])); + clice::println("{}", clice::dump(links)); } }; @@ -58,7 +58,6 @@ TEST_F(DocumentLink, Include) { run(); - auto& links = result[unit->interested_file()]; EXPECT_EQ(links.size(), 6); EXPECT_LINK(0, "0", "test.h"); EXPECT_LINK(1, "1", "test.h"); @@ -84,7 +83,6 @@ TEST_F(DocumentLink, HasInclude) { run(); - auto& links = result[unit->interested_file()]; EXPECT_EQ(links.size(), 2); EXPECT_LINK(0, "0", "test.h"); EXPECT_LINK(1, "1", "test.h"); diff --git a/xmake.lua b/xmake.lua index be845565..5dec65d5 100644 --- a/xmake.lua +++ b/xmake.lua @@ -114,7 +114,7 @@ target("unit_tests") add_tests("default") - on_config(function (target) + after_load(function (target) target:set("runargs", "--test-dir=" .. path.absolute("tests/data"), "--resource-dir=" .. path.join(target:dep("clice-core"):pkg("llvm"):installdir(), "lib/clang/20")