| 1 | // Copyright (C) 2023 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include <qqmlrangeformatting_p.h> | 
| 5 | #include <qqmlcodemodel_p.h> | 
| 6 | #include <qqmllsutils_p.h> | 
| 7 |  | 
| 8 | #include <QtQmlDom/private/qqmldomitem_p.h> | 
| 9 | #include <QtQmlDom/private/qqmldomindentinglinewriter_p.h> | 
| 10 | #include <QtQmlDom/private/qqmldomcodeformatter_p.h> | 
| 11 | #include <QtQmlDom/private/qqmldomoutwriter_p.h> | 
| 12 | #include <QtQmlDom/private/qqmldommock_p.h> | 
| 13 | #include <QtQmlDom/private/qqmldomcompare_p.h> | 
| 14 |  | 
| 15 | QT_BEGIN_NAMESPACE | 
| 16 |  | 
| 17 | Q_DECLARE_LOGGING_CATEGORY(formatLog) | 
| 18 |  | 
| 19 | QQmlRangeFormatting::QQmlRangeFormatting(QmlLsp::QQmlCodeModel *codeModel) | 
| 20 |     : QQmlBaseModule(codeModel) | 
| 21 | { | 
| 22 | } | 
| 23 |  | 
| 24 | QString QQmlRangeFormatting::name() const | 
| 25 | { | 
| 26 |     return u"QQmlRangeFormatting"_s ; | 
| 27 | } | 
| 28 |  | 
| 29 | void QQmlRangeFormatting::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) | 
| 30 | { | 
| 31 |     protocol->registerDocumentRangeFormattingRequestHandler(handler: getRequestHandler()); | 
| 32 | } | 
| 33 |  | 
| 34 | void QQmlRangeFormatting::setupCapabilities(const QLspSpecification::InitializeParams &, | 
| 35 |                                             QLspSpecification::InitializeResult &serverCapabilities) | 
| 36 | { | 
| 37 |     serverCapabilities.capabilities.documentRangeFormattingProvider = true; | 
| 38 | } | 
| 39 |  | 
| 40 | void QQmlRangeFormatting::process(RequestPointerArgument request) | 
| 41 | { | 
| 42 |     using namespace QQmlJS::Dom; | 
| 43 |     QList<QLspSpecification::TextEdit> result{}; | 
| 44 |  | 
| 45 |     QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( | 
| 46 |             url: QQmlLSUtils::lspUriToQmlUrl(uri: request->m_parameters.textDocument.uri)); | 
| 47 |  | 
| 48 |     DomItem file = doc.snapshot.doc.fileObject(option: GoTo::MostLikely); | 
| 49 |     if (!file) { | 
| 50 |         qWarning() << u"Could not find the file"_s  << doc.snapshot.doc.toString(); | 
| 51 |         return; | 
| 52 |     } | 
| 53 |  | 
| 54 |     if (auto envPtr = file.environment().ownerAs<DomEnvironment>()) | 
| 55 |         envPtr->clearReferenceCache(); | 
| 56 |  | 
| 57 |     auto qmlFile = file.ownerAs<QmlFile>(); | 
| 58 |     auto code = qmlFile->code(); | 
| 59 |  | 
| 60 |     // Range requested to be formatted | 
| 61 |     const auto selectedRange = request->m_parameters.range; | 
| 62 |     const auto selectedRangeStartLine = selectedRange.start.line; | 
| 63 |     const auto selectedRangeEndLine = selectedRange.end.line; | 
| 64 |     Q_ASSERT(selectedRangeStartLine >= 0); | 
| 65 |     Q_ASSERT(selectedRangeEndLine >= 0); | 
| 66 |  | 
| 67 |     LineWriterOptions options; | 
| 68 |     options.updateOptions = LineWriterOptions::Update::None; | 
| 69 |     options.attributesSequence = LineWriterOptions::AttributesSequence::Preserve; | 
| 70 |  | 
| 71 |     QTextStream in(&code); | 
| 72 |     FormatTextStatus status = FormatTextStatus::initialStatus(); | 
| 73 |     FormatPartialStatus partialStatus({}, options.formatOptions, status); | 
| 74 |  | 
| 75 |     // Get the token status of the previous line without performing write operation | 
| 76 |     int lineNumber = 0; | 
| 77 |     while (!in.atEnd()) { | 
| 78 |         const auto line = in.readLine(); | 
| 79 |         partialStatus = formatCodeLine(line, options: options.formatOptions, initialStatus: partialStatus.currentStatus); | 
| 80 |         if (++lineNumber >= selectedRangeStartLine) | 
| 81 |             break; | 
| 82 |     } | 
| 83 |  | 
| 84 |     QString resultText; | 
| 85 |     QTextStream out(&resultText); | 
| 86 |     IndentingLineWriter lw([&out](QStringView writtenText) { out << writtenText.toUtf8(); }, | 
| 87 |                            QString(), options, partialStatus.currentStatus); | 
| 88 |     OutWriter ow(lw); | 
| 89 |     ow.indentNextlines = true; | 
| 90 |  | 
| 91 |     // TODO: This is a workaround and will/should be handled by the actual formatter | 
| 92 |     // once we improve the range-formatter design in QTBUG-116139 | 
| 93 |     const auto removeSpaces = [](const QString &line) { | 
| 94 |         QString result; | 
| 95 |         QTextStream out(&result); | 
| 96 |         bool previousIsSpace = false; | 
| 97 |  | 
| 98 |         int newLineCount = 0; | 
| 99 |         for (int i = 0; i < line.length(); ++i) { | 
| 100 |             QChar c = line.at(i); | 
| 101 |             if (c.isSpace()) { | 
| 102 |                 if (c == '\n'_L1 && newLineCount < 2) { | 
| 103 |                     out << '\n'_L1; | 
| 104 |                     ++newLineCount; | 
| 105 |                 } else if (c == '\r'_L1 && (i + 1) < line.length() && line.at(i: i + 1) == '\n'_L1 | 
| 106 |                            && newLineCount < 2) { | 
| 107 |                     out << "\r\n" ; | 
| 108 |                     ++newLineCount; | 
| 109 |                     ++i; | 
| 110 |                 } else { | 
| 111 |                     if (!previousIsSpace) | 
| 112 |                         out << ' '_L1; | 
| 113 |                 } | 
| 114 |                 previousIsSpace = true; | 
| 115 |             } else { | 
| 116 |                 out << c; | 
| 117 |                 previousIsSpace = false; | 
| 118 |                 newLineCount = 0; | 
| 119 |             } | 
| 120 |         } | 
| 121 |  | 
| 122 |         out.flush(); | 
| 123 |         return result; | 
| 124 |     }; | 
| 125 |  | 
| 126 |     const auto startOffset = QQmlLSUtils::textOffsetFrom(code, row: selectedRangeStartLine, character: 0); | 
| 127 |     auto endOffset = QQmlLSUtils::textOffsetFrom(code, row: selectedRangeEndLine + 1, character: 0); | 
| 128 |  | 
| 129 |     // note: the character at endOffset (usually a \n) is ignored. Therefore avoid | 
| 130 |     // formatting \r\n that will ignore \n and format \r as a space (' '). | 
| 131 |     if (endOffset < code.size() && code[endOffset - 1] == u'\r' && code[endOffset] == u'\n') | 
| 132 |         --endOffset; | 
| 133 |  | 
| 134 |     const auto &toFormat = code.mid(position: startOffset, n: endOffset - startOffset); | 
| 135 |     ow.write(v: removeSpaces(toFormat)); | 
| 136 |     ow.flush(); | 
| 137 |     ow.eof(); | 
| 138 |  | 
| 139 |     const auto documentLineCount = QQmlLSUtils::textRowAndColumnFrom(code, offset: code.length()).line; | 
| 140 |     code.replace(i: startOffset, len: toFormat.length(), after: resultText); | 
| 141 |  | 
| 142 |     QLspSpecification::TextEdit add; | 
| 143 |     add.newText = code.toUtf8(); | 
| 144 |     add.range = { .start: { .line: 0, .character: 0 }, .end: { .line: documentLineCount + 1 } }; | 
| 145 |     result.append(t: add); | 
| 146 |  | 
| 147 |     request->m_response.sendResponse(r: result); | 
| 148 | } | 
| 149 |  | 
| 150 | QT_END_NAMESPACE | 
| 151 |  |