From 5f8a577e2648dbbd94f88f4a525d0f2002e29533 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 23 Aug 2025 22:52:27 +0800 Subject: [PATCH] Enable formatting (#188) --- include/AST/SourceCode.h | 4 ++ include/Feature/Formatting.h | 12 ++++++ include/Protocol/Feature/Formatting.h | 13 ++++++ include/Protocol/Lifecycle.h | 4 +- include/Server/Server.h | 22 ++++++---- src/Feature/Formatting.cpp | 60 +++++++++++++++++++++++++++ src/Server/Feature.cpp | 39 ++++++++++++++--- src/Server/Lifecycle.cpp | 4 ++ src/Server/Server.cpp | 2 + tests/unit/Feature/Formatting.cpp | 17 ++++++++ 10 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 src/Feature/Formatting.cpp create mode 100644 tests/unit/Feature/Formatting.cpp diff --git a/include/AST/SourceCode.h b/include/AST/SourceCode.h index 539407e9..9f9e0d9b 100644 --- a/include/AST/SourceCode.h +++ b/include/AST/SourceCode.h @@ -46,6 +46,10 @@ struct LocalSourceRange { constexpr bool operator== (const LocalSourceRange& other) const = default; + constexpr auto length() { + return end - begin; + } + constexpr bool contains(uint32_t offset) const { return offset >= begin && offset <= end; } diff --git a/include/Feature/Formatting.h b/include/Feature/Formatting.h index 6f70f09b..d2445a7f 100644 --- a/include/Feature/Formatting.h +++ b/include/Feature/Formatting.h @@ -1 +1,13 @@ #pragma once + +#include "AST/SourceCode.h" +#include "Protocol/Feature/Formatting.h" +#include "llvm/ADT/StringRef.h" + +namespace clice::feature { + +std::vector document_format(llvm::StringRef file, + llvm::StringRef content, + std::optional); + +} diff --git a/include/Protocol/Feature/Formatting.h b/include/Protocol/Feature/Formatting.h index 79ab3e12..f0168ee8 100644 --- a/include/Protocol/Feature/Formatting.h +++ b/include/Protocol/Feature/Formatting.h @@ -8,6 +8,19 @@ struct DocumentFormattingClientCapabilities {}; using DocumentFormattingOptions = bool; +struct DocumentFormattingParams { + /// The document to format. + TextDocumentIdentifier textDocument; +}; + +struct DocumentRangeFormattingParams { + /// The document to format. + TextDocumentIdentifier textDocument; + + /// The range to format + Range range; +}; + struct DocumentRangeFormattingClientCapabilities {}; using DocumentRangeFormattingOptions = bool; diff --git a/include/Protocol/Lifecycle.h b/include/Protocol/Lifecycle.h index 73b89572..9c804ec3 100644 --- a/include/Protocol/Lifecycle.h +++ b/include/Protocol/Lifecycle.h @@ -123,10 +123,10 @@ struct ServerCapabilities { /// FIXME: DocumentColorOptions colorProvider; /// The server provides document formatting. - /// FIXME: DocumentFormattingOptions documentFormattingProvider; + DocumentFormattingOptions documentFormattingProvider; /// The server provides document range formatting. - /// FIXME: DocumentRangeFormattingOptions documentRangeFormattingProvider; + DocumentRangeFormattingOptions documentRangeFormattingProvider; /// The server provides document formatting on typing. /// FIXME: DocumentOnTypeFormattingOptions documentOnTypeFormattingProvider; diff --git a/include/Server/Server.h b/include/Server/Server.h index abcaf3cc..e7c864e8 100644 --- a/include/Server/Server.h +++ b/include/Server/Server.h @@ -196,21 +196,27 @@ private: async::Task<> on_did_close(proto::DidCloseTextDocumentParams params); private: - async::Task on_completion(proto::CompletionParams params); + using Result = async::Task; - async::Task on_hover(proto::HoverParams params); + auto on_completion(proto::CompletionParams params) -> Result; - async::Task on_signature_help(proto::SignatureHelpParams params); + auto on_hover(proto::HoverParams params) -> Result; - async::Task on_document_symbol(proto::DocumentSymbolParams params); + auto on_signature_help(proto::SignatureHelpParams params) -> Result; - async::Task on_document_link(proto::DocumentLinkParams params); + auto on_document_symbol(proto::DocumentSymbolParams params) -> Result; - async::Task on_folding_range(proto::FoldingRangeParams params); + auto on_document_link(proto::DocumentLinkParams params) -> Result; - async::Task on_semantic_token(proto::SemanticTokensParams params); + auto on_document_format(proto::DocumentFormattingParams params) -> Result; - async::Task on_inlay_hint(proto::InlayHintParams params); + auto on_document_range_format(proto::DocumentRangeFormattingParams params) -> Result; + + auto on_folding_range(proto::FoldingRangeParams params) -> Result; + + auto on_semantic_token(proto::SemanticTokensParams params) -> Result; + + auto on_inlay_hint(proto::InlayHintParams params) -> Result; private: /// The current request id. diff --git a/src/Feature/Formatting.cpp b/src/Feature/Formatting.cpp new file mode 100644 index 00000000..360d8ab6 --- /dev/null +++ b/src/Feature/Formatting.cpp @@ -0,0 +1,60 @@ +#include "Feature/Formatting.h" +#include "Support/Logger.h" +#include "Server/Convert.h" +#include "clang/Format/Format.h" + +namespace clice::feature { + +namespace tooling = clang::tooling; + +static auto format(llvm::StringRef file, llvm::StringRef content, tooling::Range range) + -> std::expected { + /// Set the code to empty to avoid meaningless file type guess. + /// See https://github.com/llvm/llvm-project/issues/48863. + auto style = clang::format::getStyle(clang::format::DefaultFormatStyle, + file, + clang::format::DefaultFallbackStyle, + ""); + if(!style) { + return std::unexpected(std::format("{}", style.takeError())); + } + + std::vector ranges = {range}; + auto include_replacements = clang::format::sortIncludes(*style, content, ranges, file); + auto changed = tooling::applyAllReplacements(content, include_replacements); + if(!changed) { + return std::unexpected(std::format("{}", changed.takeError())); + } + + return include_replacements.merge(clang::format::reformat( + *style, + *changed, + tooling::calculateRangesAfterReplacements(include_replacements, ranges))); +} + +std::vector document_format(llvm::StringRef file, + llvm::StringRef content, + std::optional range) { + std::vector edits; + + auto selection = + range ? tooling::Range(range->begin, range->length()) : tooling::Range(0, content.size()); + auto replacements = format(file, content, selection); + if(!replacements) { + log::info("Fail to format for {}\n{}", file, replacements.error()); + return edits; + } + + for(auto& replacement: *replacements) { + proto::TextEdit edit; + PositionConverter converter(content, PositionEncodingKind::UTF8); + edit.range.start = converter.toPosition(replacement.getOffset()); + edit.range.end = converter.toPosition(replacement.getOffset() + replacement.getLength()); + edit.newText = replacement.getReplacementText(); + edits.emplace_back(std::move(edit)); + } + + return edits; +} + +} // namespace clice::feature diff --git a/src/Server/Feature.cpp b/src/Server/Feature.cpp index 8db66b83..50911f8f 100644 --- a/src/Server/Feature.cpp +++ b/src/Server/Feature.cpp @@ -1,3 +1,4 @@ +#include "Feature/Formatting.h" #include "Server/Server.h" #include "Server/Convert.h" #include "Compiler/Compilation.h" @@ -12,7 +13,7 @@ namespace clice { -async::Task Server::on_completion(proto::CompletionParams params) { +auto Server::on_completion(proto::CompletionParams params) -> Result { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); @@ -38,7 +39,7 @@ async::Task Server::on_completion(proto::CompletionParams params) { } } -async::Task Server::on_hover(proto::HoverParams params) { +auto Server::on_hover(proto::HoverParams params) -> Result { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); @@ -86,7 +87,7 @@ async::Task Server::on_signature_help(proto::SignatureHelpParams pa } } -async::Task Server::on_document_symbol(proto::DocumentSymbolParams params) { +auto Server::on_document_symbol(proto::DocumentSymbolParams params) -> Result { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); @@ -134,7 +135,7 @@ async::Task Server::on_document_symbol(proto::DocumentSymbolParams }); } -async::Task Server::on_document_link(proto::DocumentLinkParams params) { +auto Server::on_document_link(proto::DocumentLinkParams params) -> Result { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); @@ -163,6 +164,32 @@ async::Task Server::on_document_link(proto::DocumentLinkParams para }); } +auto Server::on_document_format(proto::DocumentFormattingParams params) -> Result { + auto path = mapping.to_path(params.textDocument.uri); + auto opening_file = opening_files.get_or_add(path); + + auto content = opening_file->content; + co_return co_await async::submit([&, kind = this->kind] { + auto edits = feature::document_format(path, content, std::nullopt); + /// FIXME: adjust position encoding... + return json::serialize(edits); + }); +} + +auto Server::on_document_range_format(proto::DocumentRangeFormattingParams params) -> Result { + auto path = mapping.to_path(params.textDocument.uri); + auto opening_file = opening_files.get_or_add(path); + + auto content = opening_file->content; + co_return co_await async::submit([&, kind = this->kind] { + auto begin = to_offset(kind, content, params.range.start); + auto end = to_offset(kind, content, params.range.end); + auto edits = feature::document_format(path, content, LocalSourceRange(begin, end)); + /// FIXME: adjust position encoding... + return json::serialize(edits); + }); +} + async::Task Server::on_folding_range(proto::FoldingRangeParams params) { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); @@ -201,7 +228,7 @@ async::Task Server::on_folding_range(proto::FoldingRangeParams para }); } -async::Task Server::on_semantic_token(proto::SemanticTokensParams params) { +auto Server::on_semantic_token(proto::SemanticTokensParams params) -> Result { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); @@ -217,7 +244,7 @@ async::Task Server::on_semantic_token(proto::SemanticTokensParams p }); } -async::Task Server::on_inlay_hint(proto::InlayHintParams params) { +auto Server::on_inlay_hint(proto::InlayHintParams params) -> Result { auto path = mapping.to_path(params.textDocument.uri); auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); diff --git a/src/Server/Lifecycle.cpp b/src/Server/Lifecycle.cpp index 72dd1623..8635f8f5 100644 --- a/src/Server/Lifecycle.cpp +++ b/src/Server/Lifecycle.cpp @@ -62,6 +62,10 @@ async::Task Server::on_initialize(proto::InitializeParams params) { /// DocumentLink capabilities.documentLinkProvider.resolveProvider = false; + /// Formatting + capabilities.documentFormattingProvider = true; + capabilities.documentRangeFormattingProvider = true; + /// FoldingRange capabilities.foldingRangeProvider = true; diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index 07a20d46..1f8830f9 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -111,6 +111,8 @@ Server::Server() { register_callback<&Server::on_signature_help>("textDocument/signatureHelp"); register_callback<&Server::on_document_symbol>("textDocument/documentSymbol"); register_callback<&Server::on_document_link>("textDocument/documentLink"); + register_callback<&Server::on_document_format>("textDocument/formatting"); + register_callback<&Server::on_document_range_format>("textDocument/rangeFormatting"); register_callback<&Server::on_folding_range>("textDocument/foldingRange"); register_callback<&Server::on_semantic_token>("textDocument/semanticTokens/full"); register_callback<&Server::on_inlay_hint>("textDocument/inlayHint"); diff --git a/tests/unit/Feature/Formatting.cpp b/tests/unit/Feature/Formatting.cpp new file mode 100644 index 00000000..5d9a564e --- /dev/null +++ b/tests/unit/Feature/Formatting.cpp @@ -0,0 +1,17 @@ +#include "Test/Test.h" +#include "Feature/Formatting.h" + +namespace clice::testing { + +namespace { + +suite<"Formatting"> suite = [] { + test("Simple") = [] { + auto edits = feature::document_format("main.cpp", "int main() { return 0; }", std::nullopt); + expect(ne(edits.size(), 0)); + }; +}; + +} // namespace + +} // namespace clice::testing