Commit 9f885c0b authored by Eric Armbruster's avatar Eric Armbruster
Browse files

LSP: Add expand and shrink selection actions

This commit adds support for single and multicursor LSP expand and
shrink actions via the textDocument/selectionRange message.
parent 43b2e55f
Pipeline #179326 passed with stage
in 5 minutes and 20 seconds
......@@ -37,6 +37,7 @@
#include <ktexteditor/markinterface.h>
#include <ktexteditor/movinginterface.h>
#include <ktexteditor/movingrange.h>
#include <ktexteditor_version.h>
#include <QAction>
#include <QApplication>
......@@ -464,6 +465,8 @@ class LSPClientActionView : public QObject
QPointer<QAction> m_triggerGotoSymbol;
QPointer<QAction> m_triggerFormat;
QPointer<QAction> m_triggerRename;
QPointer<QAction> m_expandSelection;
QPointer<QAction> m_shrinkSelection;
QPointer<QAction> m_complDocOn;
QPointer<QAction> m_signatureHelp;
QPointer<QAction> m_refDeclaration;
......@@ -622,6 +625,12 @@ public:
m_triggerFormat->setText(i18n("Format"));
m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename);
m_triggerRename->setText(i18n("Rename"));
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 95, 0)
m_expandSelection = actionCollection()->addAction(QStringLiteral("lspclient_expand_selection"), this, &self_type::expandSelection);
m_expandSelection->setText(i18n("Expand Selection"));
m_shrinkSelection = actionCollection()->addAction(QStringLiteral("lspclient_shrink_selection"), this, &self_type::shrinkSelection);
m_shrinkSelection->setText(i18n("Shrink Selection"));
#endif
m_switchSourceHeader = actionCollection()->addAction(QStringLiteral("lspclient_clangd_switchheader"), this, &self_type::clangdSwitchSourceHeader);
m_switchSourceHeader->setText(i18n("Switch Source Header"));
actionCollection()->setDefaultShortcut(m_switchSourceHeader, Qt::Key_F12);
......@@ -709,6 +718,10 @@ public:
menu->addAction(m_triggerRename);
menu->addAction(m_quickFix);
menu->addAction(m_requestCodeAction);
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 95, 0)
menu->addAction(m_expandSelection);
menu->addAction(m_shrinkSelection);
#endif
menu->addSeparator();
menu->addAction(m_diagnosticsSwitch);
menu->addAction(m_closeDynamic);
......@@ -2144,6 +2157,91 @@ public:
delayCancelRequest(std::move(handle));
}
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5, 95, 0)
void expandSelection()
{
changeSelection(true);
}
void shrinkSelection()
{
changeSelection(false);
}
void changeSelection(bool expand)
{
KTextEditor::View *activeView = m_mainWindow->activeView();
QPointer<KTextEditor::Document> document = activeView->document();
auto server = m_serverManager->findServer(activeView);
if (!server || !document) {
return;
}
auto h = [this, activeView, expand](const QList<std::shared_ptr<LSPSelectionRange>> &reply) {
if (reply.isEmpty()) {
showMessage(i18n("No results"), KTextEditor::Message::Information);
}
auto cursors = activeView->cursorPositions();
if (cursors.size() != reply.size()) {
showMessage(i18n("Not enough results"), KTextEditor::Message::Information);
}
auto selections = activeView->selectionRanges();
QVector<KTextEditor::Range> ret;
for (int i = 0; i < cursors.size(); i++) {
const auto &lspSelectionRange = reply.at(i);
if (lspSelectionRange) {
LSPRange currentRange = selections.isEmpty() || !selections.at(i).isValid() ? LSPRange(cursors.at(i), cursors.at(i)) : selections.at(i);
auto resultRange = findNextSelection(lspSelectionRange, currentRange, expand);
ret.append(resultRange);
} else {
ret.append(KTextEditor::Range::invalid());
}
}
activeView->setSelections(ret);
};
auto handle = server->selectionRange(document->url(), activeView->cursorPositions(), this, h);
delayCancelRequest(std::move(handle));
}
static LSPRange findNextSelection(std::shared_ptr<LSPSelectionRange> selectionRange, const LSPRange &current, bool expand)
{
if (expand) {
while (selectionRange && !selectionRange->range.contains(current)) {
selectionRange = selectionRange->parent;
}
if (selectionRange) {
if (selectionRange->range != current) {
return selectionRange->range;
} else if (selectionRange->parent) {
return selectionRange->parent->range;
}
}
} else {
std::shared_ptr<LSPSelectionRange> previous = nullptr;
while (selectionRange && current.contains(selectionRange->range) && current != selectionRange->range) {
previous = selectionRange;
selectionRange = selectionRange->parent;
}
if (previous) {
return previous->range;
}
}
return LSPRange::invalid();
}
#endif
void clangdSwitchSourceHeader()
{
KTextEditor::View *activeView = m_mainWindow->activeView();
......@@ -2769,6 +2867,7 @@ public:
bool hoverEnabled = false, highlightEnabled = false, codeActionEnabled = false;
bool formatEnabled = false;
bool renameEnabled = false;
bool selectionRangeEnabled = false;
bool isClangd = false;
bool isRustAnalyzer = false;
......@@ -2784,6 +2883,7 @@ public:
formatEnabled = caps.documentFormattingProvider || caps.documentRangeFormattingProvider;
renameEnabled = caps.renameProvider;
codeActionEnabled = caps.codeActionProvider;
selectionRangeEnabled = caps.selectionRangeProvider;
connect(server.data(), &LSPClientServer::publishDiagnostics, this, &self_type::onDiagnostics, Qt::UniqueConnection);
connect(server.data(), &LSPClientServer::applyEdit, this, &self_type::onApplyEdit, Qt::UniqueConnection);
......@@ -2848,6 +2948,12 @@ public:
if (m_requestCodeAction) {
m_requestCodeAction->setEnabled(codeActionEnabled);
}
if (m_expandSelection) {
m_expandSelection->setEnabled(selectionRangeEnabled);
}
if (m_shrinkSelection) {
m_shrinkSelection->setEnabled(selectionRangeEnabled);
}
m_switchSourceHeader->setEnabled(isClangd);
m_switchSourceHeader->setVisible(isClangd);
m_memoryUsage->setEnabled(isClangd);
......
......@@ -21,6 +21,7 @@
#include <KTextEditor/Cursor>
#include <KTextEditor/Range>
#include <memory>
#include <optional>
// Following types roughly follow the types/interfaces as defined in LSP protocol spec
......@@ -115,6 +116,7 @@ struct LSPServerCapabilities {
// workspace caps flattened
// (other parts not useful/considered at present)
LSPWorkspaceFoldersServerCapabilities workspaceFolders;
bool selectionRangeProvider = false;
};
enum class LSPMarkupKind { None = 0, PlainText = 1, MarkDown = 2 };
......@@ -220,6 +222,11 @@ struct LSPTextEdit {
QString newText;
};
struct LSPSelectionRange {
LSPRange range;
std::shared_ptr<LSPSelectionRange> parent;
};
enum class LSPCompletionItemKind {
Text = 1,
Method = 2,
......
......@@ -32,6 +32,7 @@ static const QString MEMBER_VERSION = QStringLiteral("version");
static const QString MEMBER_START = QStringLiteral("start");
static const QString MEMBER_END = QStringLiteral("end");
static const QString MEMBER_POSITION = QStringLiteral("position");
static const QString MEMBER_POSITIONS = QStringLiteral("positions");
static const QString MEMBER_LOCATION = QStringLiteral("location");
static const QString MEMBER_RANGE = QStringLiteral("range");
static const QString MEMBER_LINE = QStringLiteral("line");
......@@ -117,6 +118,15 @@ static QJsonArray to_json(const QList<LSPTextDocumentContentChangeEvent> &change
return result;
}
static QJsonArray to_json(const QVector<LSPPosition> &positions)
{
QJsonArray result;
for (const auto &position : positions) {
result.push_back(to_json(position));
}
return result;
}
static QJsonObject versionedTextDocumentIdentifier(const QUrl &document, int version = -1)
{
QJsonObject map{{MEMBER_URI, document.toString()}};
......@@ -151,6 +161,13 @@ static QJsonObject textDocumentPositionParams(const QUrl &document, LSPPosition
return params;
}
static QJsonObject textDocumentPositionsParams(const QUrl &document, const QVector<LSPPosition> &positions)
{
auto params = textDocumentParams(document);
params[MEMBER_POSITIONS] = to_json(positions);
return params;
}
static QJsonObject referenceParams(const QUrl &document, LSPPosition pos, bool decl)
{
auto params = textDocumentPositionParams(document, pos);
......@@ -372,6 +389,7 @@ static void from_json(LSPServerCapabilities &caps, const QJsonObject &json)
from_json(caps.semanticTokenProvider, json.value(QStringLiteral("semanticTokensProvider")).toObject());
auto workspace = json.value(QStringLiteral("workspace")).toObject();
from_json(caps.workspaceFolders, workspace.value(QStringLiteral("workspaceFolders")));
caps.selectionRangeProvider = toBoolOrObject(json.value(QStringLiteral("selectionRangeProvider")));
}
// follow suit; as performed in kate docmanager
......@@ -450,6 +468,37 @@ static LSPRange parseRange(const QJsonObject &range)
return {startpos, endpos};
}
static std::shared_ptr<LSPSelectionRange> parseSelectionRange(QJsonValueRef selectionRange)
{
auto current = std::make_shared<LSPSelectionRange>(LSPSelectionRange{});
std::shared_ptr<LSPSelectionRange> ret = current;
QJsonValue selRange = std::move(selectionRange);
while (selRange.isObject()) {
current->range = parseRange(selRange[MEMBER_RANGE].toObject());
if (!selRange[QStringLiteral("parent")].isObject()) {
current->parent = nullptr;
break;
}
selRange = selRange[QStringLiteral("parent")].toObject();
current->parent = std::make_shared<LSPSelectionRange>(LSPSelectionRange{});
current = current->parent;
}
return ret;
}
static QList<std::shared_ptr<LSPSelectionRange>> parseSelectionRanges(const QJsonValue &result)
{
QList<std::shared_ptr<LSPSelectionRange>> ret;
auto selectionRanges = result.toArray();
for (QJsonValueRef selectionRange : selectionRanges) {
ret.push_back(parseSelectionRange(selectionRange));
}
return ret;
}
static LSPLocation parseLocation(const QJsonObject &loc)
{
auto uri = normalizeUrl(QUrl(loc.value(MEMBER_URI).toString()));
......@@ -1275,6 +1324,7 @@ private:
{QStringLiteral("codeAction"), codeAction},
{QStringLiteral("semanticTokens"), semanticTokens},
{QStringLiteral("synchronization"), QJsonObject{{QStringLiteral("didSave"), true}}},
{QStringLiteral("selectionRange"), QJsonObject{{QStringLiteral("dynamicRegistration"), false}}},
},
},
{QStringLiteral("window"),
......@@ -1410,6 +1460,12 @@ public:
return send(init_request(QStringLiteral("textDocument/signatureHelp"), params), h);
}
RequestHandle selectionRange(const QUrl &document, const QVector<LSPPosition> &positions, const GenericReplyHandler &h)
{
auto params = textDocumentPositionsParams(document, positions);
return send(init_request(QStringLiteral("textDocument/selectionRange"), params), h);
}
RequestHandle clangdSwitchSourceHeader(const QUrl &document, const GenericReplyHandler &h)
{
auto params = QJsonObject{{MEMBER_URI, document.toString()}};
......@@ -1739,6 +1795,12 @@ LSPClientServer::signatureHelp(const QUrl &document, const LSPPosition &pos, con
return d->signatureHelp(document, pos, make_handler(h, context, parseSignatureHelp));
}
LSPClientServer::RequestHandle
LSPClientServer::selectionRange(const QUrl &document, const QVector<LSPPosition> &positions, const QObject *context, const SelectionRangeReplyHandler &h)
{
return d->selectionRange(document, positions, make_handler(h, context, parseSelectionRanges));
}
LSPClientServer::RequestHandle LSPClientServer::clangdSwitchSourceHeader(const QUrl &document, const QObject *context, const SwitchSourceHeaderHandler &h)
{
return d->clangdSwitchSourceHeader(document, make_handler(h, context, parseClangdSwitchSourceHeader));
......
......@@ -70,6 +70,7 @@ using MemoryUsageHandler = ReplyHandler<QJsonValue>;
using ExpandMacroHandler = ReplyHandler<LSPExpandedMacro>;
using SemanticTokensDeltaReplyHandler = ReplyHandler<LSPSemanticTokensDelta>;
using WorkspaceSymbolsReplyHandler = ReplyHandler<std::vector<LSPSymbolInformation>>;
using SelectionRangeReplyHandler = ReplyHandler<QList<std::shared_ptr<LSPSelectionRange>>>;
class LSPClientPlugin;
......@@ -133,7 +134,7 @@ public:
RequestHandle documentReferences(const QUrl &document, const LSPPosition &pos, bool decl, const QObject *context, const DocumentDefinitionReplyHandler &h);
RequestHandle documentCompletion(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentCompletionReplyHandler &h);
RequestHandle signatureHelp(const QUrl &document, const LSPPosition &pos, const QObject *context, const SignatureHelpReplyHandler &h);
RequestHandle selectionRange(const QUrl &document, const QVector<LSPPosition> &positions, const QObject *context, const SelectionRangeReplyHandler &h);
// clangd specific
RequestHandle clangdSwitchSourceHeader(const QUrl &document, const QObject *context, const SwitchSourceHeaderHandler &h);
RequestHandle clangdMemoryUsage(const QObject *context, const MemoryUsageHandler &h);
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE gui SYSTEM "kpartgui.dtd">
<gui name="lspclient" library="lspclient" version="20" translationDomain="lspclient">
<gui name="lspclient" library="lspclient" version="21" translationDomain="lspclient">
<MenuBar>
<Menu name="LSPClient Menubar">
<text>LSP Client</text>
......@@ -18,6 +18,8 @@
<Action name="lspclient_rename"/>
<Action name="lspclient_quick_fix"/>
<Action name="lspclient_code_action"/>
<Action name="lspclient_expand_selection"/>
<Action name="lspclient_shrink_selection"/>
<Separator/>
<Action name="lspclient_diagnostic_switch"/>
<Action name="lspclient_close_dynamic"/>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment