LanguageClient: add snippet parsing

Task-number: QTCREATORBUG-22406
Change-Id: I5b3a65984f1b4a9198bcbfec24aaa920dcb6dbf1
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
David Schulz
2021-05-11 13:37:52 +02:00
parent a80546593d
commit d729835c24
10 changed files with 359 additions and 10 deletions

View File

@@ -24,4 +24,5 @@ add_qtc_plugin(LanguageClient
lspinspector.cpp lspinspector.h lspinspector.cpp lspinspector.h
progressmanager.cpp progressmanager.h progressmanager.cpp progressmanager.h
semantichighlightsupport.cpp semantichighlightsupport.h semantichighlightsupport.cpp semantichighlightsupport.h
snippet.cpp snippet.h
) )

View File

@@ -220,7 +220,7 @@ static ClientCapabilities generateClientCapabilities()
completionCapabilities.setCompletionItemKind(completionItemKindCapabilities); completionCapabilities.setCompletionItemKind(completionItemKindCapabilities);
TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemCapbilities TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemCapbilities
completionItemCapbilities; completionItemCapbilities;
completionItemCapbilities.setSnippetSupport(false); completionItemCapbilities.setSnippetSupport(true);
completionItemCapbilities.setCommitCharacterSupport(true); completionItemCapbilities.setCommitCharacterSupport(true);
completionCapabilities.setCompletionItem(completionItemCapbilities); completionCapabilities.setCompletionItem(completionItemCapbilities);
documentCapabilities.setCompletion(completionCapabilities); documentCapabilities.setCompletion(completionCapabilities);

View File

@@ -24,7 +24,7 @@ HEADERS += \
lspinspector.h \ lspinspector.h \
progressmanager.h \ progressmanager.h \
semantichighlightsupport.h \ semantichighlightsupport.h \
snippet.h \
SOURCES += \ SOURCES += \
client.cpp \ client.cpp \
@@ -47,6 +47,7 @@ SOURCES += \
lspinspector.cpp \ lspinspector.cpp \
progressmanager.cpp \ progressmanager.cpp \
semantichighlightsupport.cpp \ semantichighlightsupport.cpp \
snippet.cpp \
RESOURCES += \ RESOURCES += \
languageclient.qrc languageclient.qrc

View File

@@ -5,6 +5,10 @@ QtcPlugin {
name: "LanguageClient" name: "LanguageClient"
Depends { name: "Qt.core" } Depends { name: "Qt.core" }
Depends {
name: "Qt.testlib"
condition: qtc.testsEnabled
}
Depends { name: "Utils" } Depends { name: "Utils" }
Depends { name: "ProjectExplorer" } Depends { name: "ProjectExplorer" }
@@ -56,6 +60,8 @@ QtcPlugin {
"progressmanager.h", "progressmanager.h",
"semantichighlightsupport.cpp", "semantichighlightsupport.cpp",
"semantichighlightsupport.h", "semantichighlightsupport.h",
"snippet.cpp",
"snippet.h",
] ]
Export { Depends { name: "LanguageServerProtocol" } } Export { Depends { name: "LanguageServerProtocol" } }

View File

@@ -27,6 +27,7 @@
#include "client.h" #include "client.h"
#include "languageclientutils.h" #include "languageclientutils.h"
#include "snippet.h"
#include <languageserverprotocol/completion.h> #include <languageserverprotocol/completion.h>
#include <texteditor/codeassist/assistinterface.h> #include <texteditor/codeassist/assistinterface.h>
@@ -108,7 +109,7 @@ void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manip
{ {
const int pos = manipulator.currentPosition(); const int pos = manipulator.currentPosition();
if (auto edit = m_item.textEdit()) { if (auto edit = m_item.textEdit()) {
applyTextEdit(manipulator, *edit); applyTextEdit(manipulator, *edit, isSnippet());
} else { } else {
const QString textToInsert(m_item.insertText().value_or(text())); const QString textToInsert(m_item.insertText().value_or(text()));
int length = 0; int length = 0;
@@ -126,8 +127,13 @@ void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manip
QRegularExpressionMatch match = identifier.match(blockTextUntilPosition); QRegularExpressionMatch match = identifier.match(blockTextUntilPosition);
int matchLength = match.hasMatch() ? match.capturedLength(0) : 0; int matchLength = match.hasMatch() ? match.capturedLength(0) : 0;
length = qMax(length, matchLength); length = qMax(length, matchLength);
if (isSnippet()) {
manipulator.replace(pos - length, length, {});
manipulator.insertCodeSnippet(pos - length, textToInsert, &parseSnippet);
} else {
manipulator.replace(pos - length, length, textToInsert); manipulator.replace(pos - length, length, textToInsert);
} }
}
if (auto additionalEdits = m_item.additionalTextEdits()) { if (auto additionalEdits = m_item.additionalTextEdits()) {
for (const auto &edit : *additionalEdits) for (const auto &edit : *additionalEdits)
@@ -182,9 +188,7 @@ QString LanguageClientCompletionItem::detail() const
bool LanguageClientCompletionItem::isSnippet() const bool LanguageClientCompletionItem::isSnippet() const
{ {
// FIXME add lsp > creator snippet converter return m_item.insertTextFormat().value_or(CompletionItem::PlainText);
// return m_item.insertTextFormat().value_or(CompletionItem::PlainText);
return false;
} }
bool LanguageClientCompletionItem::isValid() const bool LanguageClientCompletionItem::isValid() const
@@ -226,6 +230,8 @@ bool LanguageClientCompletionItem::isPerfectMatch(int pos, QTextDocument *doc) c
if (!additionalEdits.value().isEmpty()) if (!additionalEdits.value().isEmpty())
return false; return false;
} }
if (isSnippet())
return false;
if (auto edit = m_item.textEdit()) { if (auto edit = m_item.textEdit()) {
auto range = edit->range(); auto range = edit->range();
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1); const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1);

View File

@@ -51,6 +51,12 @@ private:
private: private:
LanguageClientOutlineWidgetFactory m_outlineFactory; LanguageClientOutlineWidgetFactory m_outlineFactory;
#ifdef WITH_TESTS
private slots:
void testSnippetParsing_data();
void testSnippetParsing();
#endif
}; };
} // namespace LanguageClient } // namespace LanguageClient

View File

@@ -29,6 +29,7 @@
#include "languageclient_global.h" #include "languageclient_global.h"
#include "languageclientmanager.h" #include "languageclientmanager.h"
#include "languageclientoutline.h" #include "languageclientoutline.h"
#include "snippet.h"
#include <coreplugin/editormanager/documentmodel.h> #include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
@@ -102,14 +103,21 @@ bool applyTextEdits(const DocumentUri &uri, const QList<TextEdit> &edits)
return file->apply(); return file->apply();
} }
void applyTextEdit(TextDocumentManipulatorInterface &manipulator, const TextEdit &edit) void applyTextEdit(TextDocumentManipulatorInterface &manipulator,
const TextEdit &edit,
bool newTextIsSnippet)
{ {
using namespace Utils::Text; using namespace Utils::Text;
const Range range = edit.range(); const Range range = edit.range();
const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document(); const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document();
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1); 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); 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()); manipulator.replace(start, end - start, edit.newText());
}
} }
bool applyWorkspaceEdit(const WorkspaceEdit &edit) bool applyWorkspaceEdit(const WorkspaceEdit &edit)

View File

@@ -52,7 +52,8 @@ applyTextDocumentEdit(const LanguageServerProtocol::TextDocumentEdit &edit);
bool LANGUAGECLIENT_EXPORT applyTextEdits(const LanguageServerProtocol::DocumentUri &uri, bool LANGUAGECLIENT_EXPORT applyTextEdits(const LanguageServerProtocol::DocumentUri &uri,
const QList<LanguageServerProtocol::TextEdit> &edits); const QList<LanguageServerProtocol::TextEdit> &edits);
void LANGUAGECLIENT_EXPORT applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, void LANGUAGECLIENT_EXPORT applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator,
const LanguageServerProtocol::TextEdit &edit); const LanguageServerProtocol::TextEdit &edit,
bool newTextIsSnippet = false);
void LANGUAGECLIENT_EXPORT void LANGUAGECLIENT_EXPORT
updateCodeActionRefactoringMarker(Client *client, updateCodeActionRefactoringMarker(Client *client,
const LanguageServerProtocol::CodeAction &action, const LanguageServerProtocol::CodeAction &action,

View 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

View 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