From 7eacd2664fc1b6b51861647393438b3c6af8bb86 Mon Sep 17 00:00:00 2001 From: Fangrui Song Date: Thu, 27 Sep 2018 22:19:26 -0700 Subject: [PATCH] Support textDocument/{formatting,onTypeFormatting,rangeFormatting} --- CMakeLists.txt | 1 + README.md | 3 +- cmake/FindClang.cmake | 10 ++ src/messages/initialize.cc | 11 +- src/messages/textDocument_formatting.cc | 192 ++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 src/messages/textDocument_formatting.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index fca0ff41..384c008f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,6 +222,7 @@ target_sources(ccls PRIVATE src/messages/textDocument_completion.cc src/messages/textDocument_definition.cc src/messages/textDocument_did.cc + src/messages/textDocument_formatting.cc src/messages/textDocument_documentHighlight.cc src/messages/textDocument_documentSymbol.cc src/messages/textDocument_hover.cc diff --git a/README.md b/README.md index e3349def..2a0c70fa 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ccls, which originates from [cquery](https://github.com/cquery-project/cquery), * code completion (with both signature help and snippets) * [definition](src/messages/textDocument_definition.cc)/[references](src/messages/textDcument_references.cc), and other cross references * cross reference extensions: `$ccls/call` `$ccls/inheritance` `$ccls/member` `$ccls/vars` ... + * formatting * hierarchies: [call (caller/callee) hierarchy](src/messages/ccls_call.cc), [inheritance (base/derived) hierarchy](src/messages/ccls_inheritance.cc), [member hierarchy](src/messages/ccls_member.cc) * [symbol rename](src/messages/textDocument_rename.cc) * [document symbols](src/messages/textDocument_documentSymbol.cc) and approximate search of [workspace symbol](src/messages/workspace_symbol.cc) @@ -22,7 +23,7 @@ Saving files will incrementally update the index. Compared with cquery, it makes use of C++17 features, has less third-party dependencies and slimmed-down code base. It leverages Clang C++ API as [clangd](https://clang.llvm.org/extra/clangd.html) does, which provides better support for code completion and diagnostics. -Refactoring and formatting are non-goals as they can be provided by clang-format, clang-include-fixer and other Clang based tools. +Refactoring is a non-goal as it can be provided by clang-include-fixer and other Clang based tools. The comparison with cquery as noted on 2018-07-15: diff --git a/cmake/FindClang.cmake b/cmake/FindClang.cmake index e4286d51..e56e25dd 100644 --- a/cmake/FindClang.cmake +++ b/cmake/FindClang.cmake @@ -69,7 +69,17 @@ set(_Clang_REQUIRED_VARS Clang_LIBRARY Clang_INCLUDE_DIR Clang_EXECUTABLE LLVM_INCLUDE_DIR LLVM_BUILD_INCLUDE_DIR) _Clang_find_library(Clang_LIBRARY clangIndex) +_Clang_find_add_library(clangFormat) _Clang_find_add_library(clangTooling) + +# TODO Remove after dropping clang 6 +_Clang_find_library(clangToolingInclusions_LIBRARY clangToolingInclusions) +if (clangToolingInclusions_LIBRARY) + list(APPEND _Clang_LIBRARIES ${clangToolingInclusions_LIBRARY}) +endif() + +_Clang_find_add_library(clangToolingCore) +_Clang_find_add_library(clangRewrite) _Clang_find_add_library(clangFrontend) _Clang_find_add_library(clangParse) _Clang_find_add_library(clangSerialization) diff --git a/src/messages/initialize.cc b/src/messages/initialize.cc index 03151efb..74fdaf94 100644 --- a/src/messages/initialize.cc +++ b/src/messages/initialize.cc @@ -54,7 +54,7 @@ MAKE_REFLECT_STRUCT(lsCompletionOptions, resolveProvider, triggerCharacters); // Format document on type options struct lsDocumentOnTypeFormattingOptions { // A character on which formatting should be triggered, like `}`. - std::string firstTriggerCharacter; + std::string firstTriggerCharacter = "}"; // More trigger characters. std::vector moreTriggerCharacter; @@ -65,7 +65,7 @@ MAKE_REFLECT_STRUCT(lsDocumentOnTypeFormattingOptions, firstTriggerCharacter, // Document link options struct lsDocumentLinkOptions { // Document links have a resolve provider as well. - bool resolveProvider = true; + bool resolveProvider = false; }; MAKE_REFLECT_STRUCT(lsDocumentLinkOptions, resolveProvider); @@ -158,12 +158,11 @@ struct lsServerCapabilities { // The server provides code lens. lsCodeLensOptions codeLensProvider; // The server provides document formatting. - bool documentFormattingProvider = false; + bool documentFormattingProvider = true; // The server provides document range formatting. - bool documentRangeFormattingProvider = false; + bool documentRangeFormattingProvider = true; // The server provides document formatting on typing. - std::optional - documentOnTypeFormattingProvider; + lsDocumentOnTypeFormattingOptions documentOnTypeFormattingProvider; // The server provides rename support. bool renameProvider = true; // The server provides document link support. diff --git a/src/messages/textDocument_formatting.cc b/src/messages/textDocument_formatting.cc new file mode 100644 index 00000000..b776efe4 --- /dev/null +++ b/src/messages/textDocument_formatting.cc @@ -0,0 +1,192 @@ +// Copyright 2017-2018 ccls Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "message_handler.h" +#include "pipeline.hh" +#include "working_files.h" + +#include +#include + +using namespace ccls; +using namespace clang; + +namespace { +const MethodType formatting = "textDocument/formatting", + onTypeFormatting = "textDocument/onTypeFormatting", + rangeFormatting = "textDocument/rangeFormatting"; + +struct lsFormattingOptions { + // Size of a tab in spaces. + int tabSize; + // Prefer spaces over tabs. + bool insertSpaces; +}; +MAKE_REFLECT_STRUCT(lsFormattingOptions, tabSize, insertSpaces); + +struct Out_TextDocumentFormatting + : public lsOutMessage { + lsRequestId id; + std::vector result; +}; +MAKE_REFLECT_STRUCT(Out_TextDocumentFormatting, jsonrpc, id, result); + +llvm::Expected +FormatCode(std::string_view code, std::string_view file, tooling::Range Range) { + StringRef Code(code.data(), code.size()), File(file.data(), file.size()); + auto Style = format::getStyle("file", File, "LLVM", Code, nullptr); + if (!Style) + return Style.takeError(); + tooling::Replacements IncludeReplaces = + format::sortIncludes(*Style, Code, {Range}, File); + auto Changed = tooling::applyAllReplacements(Code, IncludeReplaces); + if (!Changed) + return Changed.takeError(); + return IncludeReplaces.merge(format::reformat( + *Style, *Changed, + tooling::calculateRangesAfterReplacements(IncludeReplaces, {Range}), + File)); +} + +std::vector +ReplacementsToEdits(std::string_view code, const tooling::Replacements &Repls) { + std::vector ret; + int i = 0, line = 0, col = 0; + auto move = [&](int p) { + for (; i < p; i++) + if (code[i] == '\n') + line++, col = 0; + else { + if ((uint8_t)code[i] >= 128) { + while (128 <= (uint8_t)code[++i] && (uint8_t)code[i] < 192) + ; + i--; + } + col++; + } + }; + for (const auto &R : Repls) { + move(R.getOffset()); + int l = line, c = col; + move(R.getOffset() + R.getLength()); + ret.push_back({{{l, c}, {line, col}}, R.getReplacementText().str()}); + } + return ret; +} + +void Format(WorkingFile *wfile, tooling::Range range, lsRequestId id) { + std::string_view code = wfile->buffer_content; + auto ReplsOrErr = + FormatCode(code, wfile->filename, range); + if (ReplsOrErr) { + Out_TextDocumentFormatting out; + out.id = id; + out.result = ReplacementsToEdits(code, *ReplsOrErr); + pipeline::WriteStdout(formatting, out); + } else { + Out_Error err; + err.id = id; + err.error.code = lsErrorCodes::UnknownErrorCode; + err.error.message = llvm::toString(ReplsOrErr.takeError()); + pipeline::WriteStdout(kMethodType_Unknown, err); + } +} + +struct In_TextDocumentFormatting : public RequestInMessage { + MethodType GetMethodType() const override { return formatting; } + struct Params { + lsTextDocumentIdentifier textDocument; + lsFormattingOptions options; + } params; +}; +MAKE_REFLECT_STRUCT(In_TextDocumentFormatting::Params, textDocument, options); +MAKE_REFLECT_STRUCT(In_TextDocumentFormatting, id, params); +REGISTER_IN_MESSAGE(In_TextDocumentFormatting); + +struct Handler_TextDocumentFormatting + : BaseMessageHandler { + MethodType GetMethodType() const override { return formatting; } + void Run(In_TextDocumentFormatting *request) override { + auto ¶ms = request->params; + QueryFile *file; + if (!FindFileOrFail(db, project, request->id, + params.textDocument.uri.GetPath(), &file)) + return; + WorkingFile *wfile = working_files->GetFileByFilename(file->def->path); + if (!wfile) + return; + Format(wfile, {0, (unsigned)wfile->buffer_content.size()}, request->id); + } +}; +REGISTER_MESSAGE_HANDLER(Handler_TextDocumentFormatting); + +struct In_TextDocumentOnTypeFormatting : public RequestInMessage { + MethodType GetMethodType() const override { return onTypeFormatting; } + struct Params { + lsTextDocumentIdentifier textDocument; + lsPosition position; + std::string ch; + lsFormattingOptions options; + } params; +}; +MAKE_REFLECT_STRUCT(In_TextDocumentOnTypeFormatting::Params, textDocument, + position, ch, options); +MAKE_REFLECT_STRUCT(In_TextDocumentOnTypeFormatting, id, params); +REGISTER_IN_MESSAGE(In_TextDocumentOnTypeFormatting); + +struct Handler_TextDocumentOnTypeFormatting + : BaseMessageHandler { + MethodType GetMethodType() const override { return onTypeFormatting; } + void Run(In_TextDocumentOnTypeFormatting *request) override { + auto ¶ms = request->params; + QueryFile *file; + if (!FindFileOrFail(db, project, request->id, + params.textDocument.uri.GetPath(), &file)) + return; + WorkingFile *wfile = working_files->GetFileByFilename(file->def->path); + if (!wfile) + return; + std::string_view code = wfile->buffer_content; + int pos = GetOffsetForPosition(params.position, code); + auto lbrace = code.find_last_of('{', pos); + if (lbrace == std::string::npos) + lbrace = pos; + Format(wfile, {(unsigned)lbrace, unsigned(pos - lbrace)}, request->id); + } +}; +REGISTER_MESSAGE_HANDLER(Handler_TextDocumentOnTypeFormatting); + +struct In_TextDocumentRangeFormatting : public RequestInMessage { + MethodType GetMethodType() const override { return rangeFormatting; } + struct Params { + lsTextDocumentIdentifier textDocument; + lsRange range; + lsFormattingOptions options; + } params; +}; +MAKE_REFLECT_STRUCT(In_TextDocumentRangeFormatting::Params, textDocument, range, + options); +MAKE_REFLECT_STRUCT(In_TextDocumentRangeFormatting, id, params); +REGISTER_IN_MESSAGE(In_TextDocumentRangeFormatting); + +struct Handler_TextDocumentRangeFormatting + : BaseMessageHandler { + MethodType GetMethodType() const override { return rangeFormatting; } + + void Run(In_TextDocumentRangeFormatting *request) override { + auto ¶ms = request->params; + QueryFile *file; + if (!FindFileOrFail(db, project, request->id, + params.textDocument.uri.GetPath(), &file)) + return; + WorkingFile *wfile = working_files->GetFileByFilename(file->def->path); + if (!wfile) + return; + std::string_view code = wfile->buffer_content; + int begin = GetOffsetForPosition(params.range.start, code), + end = GetOffsetForPosition(params.range.end, code); + Format(wfile, {(unsigned)begin, unsigned(end - begin)}, request->id); + } +}; +REGISTER_MESSAGE_HANDLER(Handler_TextDocumentRangeFormatting); +} // namespace