forked from qt-creator/qt-creator
LanguageClient: add snippet parsing
Task-number: QTCREATORBUG-22406 Change-Id: I5b3a65984f1b4a9198bcbfec24aaa920dcb6dbf1 Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
@@ -24,4 +24,5 @@ add_qtc_plugin(LanguageClient
|
||||
lspinspector.cpp lspinspector.h
|
||||
progressmanager.cpp progressmanager.h
|
||||
semantichighlightsupport.cpp semantichighlightsupport.h
|
||||
snippet.cpp snippet.h
|
||||
)
|
||||
|
||||
@@ -220,7 +220,7 @@ static ClientCapabilities generateClientCapabilities()
|
||||
completionCapabilities.setCompletionItemKind(completionItemKindCapabilities);
|
||||
TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemCapbilities
|
||||
completionItemCapbilities;
|
||||
completionItemCapbilities.setSnippetSupport(false);
|
||||
completionItemCapbilities.setSnippetSupport(true);
|
||||
completionItemCapbilities.setCommitCharacterSupport(true);
|
||||
completionCapabilities.setCompletionItem(completionItemCapbilities);
|
||||
documentCapabilities.setCompletion(completionCapabilities);
|
||||
|
||||
@@ -24,7 +24,7 @@ HEADERS += \
|
||||
lspinspector.h \
|
||||
progressmanager.h \
|
||||
semantichighlightsupport.h \
|
||||
|
||||
snippet.h \
|
||||
|
||||
SOURCES += \
|
||||
client.cpp \
|
||||
@@ -47,6 +47,7 @@ SOURCES += \
|
||||
lspinspector.cpp \
|
||||
progressmanager.cpp \
|
||||
semantichighlightsupport.cpp \
|
||||
snippet.cpp \
|
||||
|
||||
RESOURCES += \
|
||||
languageclient.qrc
|
||||
|
||||
@@ -5,6 +5,10 @@ QtcPlugin {
|
||||
name: "LanguageClient"
|
||||
|
||||
Depends { name: "Qt.core" }
|
||||
Depends {
|
||||
name: "Qt.testlib"
|
||||
condition: qtc.testsEnabled
|
||||
}
|
||||
|
||||
Depends { name: "Utils" }
|
||||
Depends { name: "ProjectExplorer" }
|
||||
@@ -56,6 +60,8 @@ QtcPlugin {
|
||||
"progressmanager.h",
|
||||
"semantichighlightsupport.cpp",
|
||||
"semantichighlightsupport.h",
|
||||
"snippet.cpp",
|
||||
"snippet.h",
|
||||
]
|
||||
|
||||
Export { Depends { name: "LanguageServerProtocol" } }
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "client.h"
|
||||
#include "languageclientutils.h"
|
||||
#include "snippet.h"
|
||||
|
||||
#include <languageserverprotocol/completion.h>
|
||||
#include <texteditor/codeassist/assistinterface.h>
|
||||
@@ -108,7 +109,7 @@ void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manip
|
||||
{
|
||||
const int pos = manipulator.currentPosition();
|
||||
if (auto edit = m_item.textEdit()) {
|
||||
applyTextEdit(manipulator, *edit);
|
||||
applyTextEdit(manipulator, *edit, isSnippet());
|
||||
} else {
|
||||
const QString textToInsert(m_item.insertText().value_or(text()));
|
||||
int length = 0;
|
||||
@@ -126,8 +127,13 @@ void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manip
|
||||
QRegularExpressionMatch match = identifier.match(blockTextUntilPosition);
|
||||
int matchLength = match.hasMatch() ? match.capturedLength(0) : 0;
|
||||
length = qMax(length, matchLength);
|
||||
if (isSnippet()) {
|
||||
manipulator.replace(pos - length, length, {});
|
||||
manipulator.insertCodeSnippet(pos - length, textToInsert, &parseSnippet);
|
||||
} else {
|
||||
manipulator.replace(pos - length, length, textToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto additionalEdits = m_item.additionalTextEdits()) {
|
||||
for (const auto &edit : *additionalEdits)
|
||||
@@ -182,9 +188,7 @@ QString LanguageClientCompletionItem::detail() const
|
||||
|
||||
bool LanguageClientCompletionItem::isSnippet() const
|
||||
{
|
||||
// FIXME add lsp > creator snippet converter
|
||||
// return m_item.insertTextFormat().value_or(CompletionItem::PlainText);
|
||||
return false;
|
||||
return m_item.insertTextFormat().value_or(CompletionItem::PlainText);
|
||||
}
|
||||
|
||||
bool LanguageClientCompletionItem::isValid() const
|
||||
@@ -226,6 +230,8 @@ bool LanguageClientCompletionItem::isPerfectMatch(int pos, QTextDocument *doc) c
|
||||
if (!additionalEdits.value().isEmpty())
|
||||
return false;
|
||||
}
|
||||
if (isSnippet())
|
||||
return false;
|
||||
if (auto edit = m_item.textEdit()) {
|
||||
auto range = edit->range();
|
||||
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1);
|
||||
|
||||
@@ -51,6 +51,12 @@ private:
|
||||
|
||||
private:
|
||||
LanguageClientOutlineWidgetFactory m_outlineFactory;
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
private slots:
|
||||
void testSnippetParsing_data();
|
||||
void testSnippetParsing();
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace LanguageClient
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "languageclient_global.h"
|
||||
#include "languageclientmanager.h"
|
||||
#include "languageclientoutline.h"
|
||||
#include "snippet.h"
|
||||
|
||||
#include <coreplugin/editormanager/documentmodel.h>
|
||||
#include <coreplugin/icore.h>
|
||||
@@ -102,14 +103,21 @@ bool applyTextEdits(const DocumentUri &uri, const QList<TextEdit> &edits)
|
||||
return file->apply();
|
||||
}
|
||||
|
||||
void applyTextEdit(TextDocumentManipulatorInterface &manipulator, const TextEdit &edit)
|
||||
void applyTextEdit(TextDocumentManipulatorInterface &manipulator,
|
||||
const TextEdit &edit,
|
||||
bool newTextIsSnippet)
|
||||
{
|
||||
using namespace Utils::Text;
|
||||
const Range range = edit.range();
|
||||
const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document();
|
||||
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1);
|
||||
const int end = positionInText(doc, range.end().line() + 1, range.end().character() + 1);
|
||||
if (newTextIsSnippet) {
|
||||
manipulator.replace(start, end - start, {});
|
||||
manipulator.insertCodeSnippet(start, edit.newText(), &parseSnippet);
|
||||
} else {
|
||||
manipulator.replace(start, end - start, edit.newText());
|
||||
}
|
||||
}
|
||||
|
||||
bool applyWorkspaceEdit(const WorkspaceEdit &edit)
|
||||
|
||||
@@ -52,7 +52,8 @@ applyTextDocumentEdit(const LanguageServerProtocol::TextDocumentEdit &edit);
|
||||
bool LANGUAGECLIENT_EXPORT applyTextEdits(const LanguageServerProtocol::DocumentUri &uri,
|
||||
const QList<LanguageServerProtocol::TextEdit> &edits);
|
||||
void LANGUAGECLIENT_EXPORT applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator,
|
||||
const LanguageServerProtocol::TextEdit &edit);
|
||||
const LanguageServerProtocol::TextEdit &edit,
|
||||
bool newTextIsSnippet = false);
|
||||
void LANGUAGECLIENT_EXPORT
|
||||
updateCodeActionRefactoringMarker(Client *client,
|
||||
const LanguageServerProtocol::CodeAction &action,
|
||||
|
||||
284
src/plugins/languageclient/snippet.cpp
Normal file
284
src/plugins/languageclient/snippet.cpp
Normal file
@@ -0,0 +1,284 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2021 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "snippet.h"
|
||||
|
||||
#include "languageclientplugin.h"
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
#include <QtTest>
|
||||
#endif
|
||||
|
||||
using namespace TextEditor;
|
||||
|
||||
namespace LanguageClient {
|
||||
|
||||
constexpr char dollar = '$';
|
||||
constexpr char backSlash = '\\';
|
||||
constexpr char underscore = '_';
|
||||
constexpr char comma = ',';
|
||||
constexpr char openBrace = '{';
|
||||
constexpr char closeBrace = '}';
|
||||
constexpr char pipe = '|';
|
||||
constexpr char colon = ':';
|
||||
|
||||
class SnippetParseException
|
||||
{
|
||||
public:
|
||||
QString message;
|
||||
};
|
||||
|
||||
void skipSpaces(QString::const_iterator &it)
|
||||
{
|
||||
while (it->isSpace())
|
||||
++it;
|
||||
}
|
||||
|
||||
QString join(const QList<QChar> &chars)
|
||||
{
|
||||
QString result;
|
||||
const QList<QChar>::const_iterator begin = chars.begin();
|
||||
const QList<QChar>::const_iterator end = chars.end();
|
||||
for (QList<QChar>::const_iterator it = begin; it < end; ++it) {
|
||||
if (it == begin)
|
||||
result += "'";
|
||||
else if (it + 1 == end)
|
||||
result += ", or '";
|
||||
else
|
||||
result += ", '";
|
||||
result += *it + "'";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool checkChars(QString::const_iterator &it, const QList<QChar> &chars)
|
||||
{
|
||||
if (*it == backSlash) {
|
||||
++it;
|
||||
if (!chars.contains(*it))
|
||||
throw SnippetParseException{"expected " + join(chars) + "after escaping '\\'"};
|
||||
return false;
|
||||
}
|
||||
return chars.contains(*it);
|
||||
}
|
||||
|
||||
void skipToEndOfTabstop(QString::const_iterator &it, const QString::const_iterator &end)
|
||||
{
|
||||
while (it < end && checkChars(it, {closeBrace}))
|
||||
++it;
|
||||
}
|
||||
|
||||
int parseTabstopIndex(QString::const_iterator &it)
|
||||
{
|
||||
int result = 0;
|
||||
while (it->isDigit()) {
|
||||
result = 10 * result + it->digitValue();
|
||||
++it;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString parseVariable(QString::const_iterator &it)
|
||||
{
|
||||
// TODO: implement replacing variable with data
|
||||
QString result;
|
||||
const QString::const_iterator start = it;
|
||||
while (it->isLetter() || *it == underscore || (start != it && it->isDigit())) {
|
||||
result.append(*it);
|
||||
++it;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ParsedSnippet::Part parseTabstop(QString::const_iterator &it, const QString::const_iterator &end)
|
||||
{
|
||||
ParsedSnippet::Part result;
|
||||
if (*it != dollar)
|
||||
throw SnippetParseException{"Expected a '$' (tabstop)"};
|
||||
skipSpaces(++it);
|
||||
if (it->isDigit()) {
|
||||
result.variableIndex = parseTabstopIndex(it);
|
||||
if (result.variableIndex == 0)
|
||||
result.finalPart = true;
|
||||
} else if (*it == openBrace) {
|
||||
skipSpaces(++it);
|
||||
if (it->isDigit()) {
|
||||
result.variableIndex = parseTabstopIndex(it);
|
||||
if (result.variableIndex == 0)
|
||||
result.finalPart = true;
|
||||
skipSpaces(it);
|
||||
if (*it == colon) {
|
||||
++it;
|
||||
while (it < end && !checkChars(it, {closeBrace})) {
|
||||
result.text.append(*it);
|
||||
++it;
|
||||
}
|
||||
} else if (*it == pipe) {
|
||||
++it;
|
||||
// TODO: Implement Choices for now take the first choice and use it as a placeholder
|
||||
for (; it < end && !checkChars(it, {comma, pipe, closeBrace}); ++it)
|
||||
result.text.append(*it);
|
||||
skipToEndOfTabstop(it, end);
|
||||
}
|
||||
} else if (it->isLetter() || *it == underscore) {
|
||||
result.text = parseVariable(it);
|
||||
// TODO: implement variable transformation
|
||||
skipToEndOfTabstop(it, end);
|
||||
}
|
||||
if (*it != closeBrace)
|
||||
throw SnippetParseException{"Expected a closing curly brace"};
|
||||
++it;
|
||||
} else if (it->isLetter() || *it == underscore) {
|
||||
result.text = parseVariable(it);
|
||||
} else {
|
||||
throw SnippetParseException{"Expected tabstop index, variable, or open curly brace"};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SnippetParseResult parseSnippet(const QString &snippet)
|
||||
{
|
||||
ParsedSnippet result;
|
||||
ParsedSnippet::Part currentPart;
|
||||
|
||||
Utils::optional<QString> error;
|
||||
auto it = snippet.begin();
|
||||
const auto end = snippet.end();
|
||||
|
||||
while (it < end) {
|
||||
try {
|
||||
if (checkChars(it, {dollar})) {
|
||||
if (!currentPart.text.isEmpty()) {
|
||||
if (currentPart.variableIndex != -1) {
|
||||
throw SnippetParseException{
|
||||
"Internal Error: expected variable index -1 in snippet part"};
|
||||
}
|
||||
result.parts.append(currentPart);
|
||||
currentPart.text.clear();
|
||||
}
|
||||
const ParsedSnippet::Part &part = parseTabstop(it, end);
|
||||
while (result.variables.size() < part.variableIndex + 1)
|
||||
result.variables.append(QList<int>());
|
||||
result.variables[part.variableIndex] << result.parts.size();
|
||||
result.parts.append(part);
|
||||
} else {
|
||||
currentPart.text.append(*it);
|
||||
++it;
|
||||
}
|
||||
} catch (const SnippetParseException &e) {
|
||||
return SnippetParseError{e.message, snippet, int(it - snippet.begin())};
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentPart.text.isEmpty())
|
||||
result.parts.append(currentPart);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace LanguageClient
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
|
||||
const char NOMANGLER_ID[] = "TextEditor::NoMangler";
|
||||
|
||||
struct SnippetPart
|
||||
{
|
||||
SnippetPart() = default;
|
||||
explicit SnippetPart(const QString &text,
|
||||
int index = -1,
|
||||
const Utils::Id &manglerId = NOMANGLER_ID,
|
||||
const QList<SnippetPart> &nested = {})
|
||||
: text(text)
|
||||
, variableIndex(index)
|
||||
, manglerId(manglerId)
|
||||
, nested(nested)
|
||||
{}
|
||||
QString text;
|
||||
int variableIndex = -1; // if variable index is >= 0 the text is interpreted as a variable
|
||||
Utils::Id manglerId;
|
||||
QList<SnippetPart> nested;
|
||||
};
|
||||
Q_DECLARE_METATYPE(SnippetPart);
|
||||
|
||||
using Parts = QList<SnippetPart>;
|
||||
void LanguageClient::LanguageClientPlugin::testSnippetParsing_data()
|
||||
{
|
||||
QTest::addColumn<QString>("input");
|
||||
QTest::addColumn<bool>("success");
|
||||
QTest::addColumn<Parts>("parts");
|
||||
|
||||
QTest::newRow("no input") << QString() << true << Parts();
|
||||
QTest::newRow("empty input") << QString("") << true << Parts();
|
||||
|
||||
QTest::newRow("empty tabstop") << QString("$1") << true << Parts{SnippetPart("", 1)};
|
||||
QTest::newRow("empty tabstop with braces") << QString("${1}") << true << Parts{SnippetPart("", 1)};
|
||||
QTest::newRow("double tabstop")
|
||||
<< QString("$1$1") << true << Parts{SnippetPart("", 1), SnippetPart("", 1)};
|
||||
QTest::newRow("different tabstop")
|
||||
<< QString("$1$2") << true << Parts{SnippetPart("", 1), SnippetPart("", 2)};
|
||||
QTest::newRow("empty tabstop") << QString("$1") << true << Parts{SnippetPart("", 1)};
|
||||
QTest::newRow("double dollar") << QString("$$1") << false << Parts();
|
||||
QTest::newRow("escaped tabstop") << QString("\\$1") << true << Parts{SnippetPart("$1")};
|
||||
QTest::newRow("escaped double tabstop")
|
||||
<< QString("\\$$1") << true << Parts{SnippetPart("$"), SnippetPart("", 1)};
|
||||
|
||||
QTest::newRow("placeholder") << QString("${1:foo}") << true << Parts{SnippetPart("foo", 1)};
|
||||
QTest::newRow("placeholder with text")
|
||||
<< QString("text${1:foo}text") << true
|
||||
<< Parts{SnippetPart("text"), SnippetPart("foo", 1), SnippetPart("text")};
|
||||
QTest::newRow("2 placeholder") << QString("${1:foo}${2:bar}") << true
|
||||
<< Parts{SnippetPart("foo", 1), SnippetPart("bar", 2)};
|
||||
QTest::newRow("2 placeholder same tabstop")
|
||||
<< QString("${1:foo}${1:bar}") << true
|
||||
<< Parts{SnippetPart("foo", 1), SnippetPart("bar", 1)};
|
||||
}
|
||||
|
||||
void LanguageClient::LanguageClientPlugin::testSnippetParsing()
|
||||
{
|
||||
QFETCH(QString, input);
|
||||
QFETCH(bool, success);
|
||||
QFETCH(Parts, parts);
|
||||
|
||||
SnippetParseResult result = LanguageClient::parseSnippet(input);
|
||||
QCOMPARE(Utils::holds_alternative<ParsedSnippet>(result), success);
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
ParsedSnippet snippet = Utils::get<ParsedSnippet>(result);
|
||||
|
||||
auto rangesCompare = [&](const ParsedSnippet::Part &actual, const SnippetPart &expected) {
|
||||
QCOMPARE(actual.text, expected.text);
|
||||
QCOMPARE(actual.variableIndex, expected.variableIndex);
|
||||
auto manglerId = actual.mangler ? actual.mangler->id() : NOMANGLER_ID;
|
||||
QCOMPARE(manglerId, expected.manglerId);
|
||||
};
|
||||
|
||||
QCOMPARE(snippet.parts.count(), parts.count());
|
||||
|
||||
for (int i = 0; i < parts.count(); ++i)
|
||||
rangesCompare(snippet.parts.at(i), parts.at(i));
|
||||
}
|
||||
#endif
|
||||
36
src/plugins/languageclient/snippet.h
Normal file
36
src/plugins/languageclient/snippet.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2021 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "languageclient_global.h"
|
||||
|
||||
#include <texteditor/snippets/snippetparser.h>
|
||||
|
||||
namespace LanguageClient {
|
||||
|
||||
LANGUAGECLIENT_EXPORT TextEditor::SnippetParseResult parseSnippet(const QString &snippet);
|
||||
|
||||
} // namespace LanguageClient
|
||||
Reference in New Issue
Block a user