diff --git a/addons/lspclient/lspclientserver.cpp b/addons/lspclient/lspclientserver.cpp index 9fee9fb06..208f8b840 100644 --- a/addons/lspclient/lspclientserver.cpp +++ b/addons/lspclient/lspclientserver.cpp @@ -1,911 +1,908 @@ /*************************************************************************** * Copyright (C) 2019 by Mark Nauwelaerts * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "lspclientserver.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include #include #include static const QString MEMBER_ID = QStringLiteral("id"); static const QString MEMBER_METHOD = QStringLiteral("method"); static const QString MEMBER_ERROR = QStringLiteral("error"); static const QString MEMBER_CODE = QStringLiteral("code"); static const QString MEMBER_MESSAGE = QStringLiteral("message"); static const QString MEMBER_PARAMS = QStringLiteral("params"); static const QString MEMBER_RESULT = QStringLiteral("result"); static const QString MEMBER_URI = QStringLiteral("uri"); 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_LOCATION = QStringLiteral("location"); static const QString MEMBER_RANGE = QStringLiteral("range"); static const QString MEMBER_LINE = QStringLiteral("line"); static const QString MEMBER_CHARACTER = QStringLiteral("character"); static const QString MEMBER_KIND = QStringLiteral("kind"); static const QString MEMBER_TEXT = QStringLiteral("text"); static const QString MEMBER_LANGID = QStringLiteral("languageId"); static const QString MEMBER_LABEL = QStringLiteral("label"); static const QString MEMBER_DOCUMENTATION = QStringLiteral("documentation"); static const QString MEMBER_DETAIL = QStringLiteral("detail"); // message construction helpers static QJsonObject versionedTextDocumentIdentifier(const QUrl & document, int version = -1) { QJsonObject map { { MEMBER_URI, document.toString() } }; if (version >= 0) map[MEMBER_VERSION] = version; return map; } static QJsonObject textDocumentItem(const QUrl & document, const QString & lang, const QString & text, int version) { auto map = versionedTextDocumentIdentifier(document, version); map[MEMBER_TEXT] = text; // TODO ?? server does not mind map[MEMBER_LANGID] = lang; return map; } static QJsonObject textDocumentParams(const QJsonObject & m) { return QJsonObject { { QStringLiteral("textDocument"), m} }; } static QJsonObject textDocumentParams(const QUrl & document, int version = -1) { return textDocumentParams(versionedTextDocumentIdentifier(document, version)); } static QJsonObject textDocumentPositionParams(const QUrl & document, LSPPosition pos) { auto params = textDocumentParams(document); params[MEMBER_POSITION] = QJsonObject { { MEMBER_LINE, pos.line() }, { MEMBER_CHARACTER, pos.column() } }; return params; } static QJsonObject referenceParams(const QUrl & document, LSPPosition pos, bool decl) { auto params = textDocumentPositionParams(document, pos); params[QStringLiteral("context")] = QJsonObject { { QStringLiteral("includeDeclaration"), decl } }; return params; } static void from_json(QVector & trigger, const QJsonValue & json) { for (const auto & t : json.toArray()) { auto st = t.toString(); if (st.length()) trigger.push_back(st.at(0)); } } static void from_json(LSPCompletionOptions & options, const QJsonValue & json) { if (json.isObject()) { auto ob = json.toObject(); options.provider = true; options.resolveProvider = ob.value(QStringLiteral("resolveProvider")).toBool(); from_json(options.triggerCharacters, ob.value(QStringLiteral("triggerCharacters"))); } } static void from_json(LSPSignatureHelpOptions & options, const QJsonValue & json) { if (json.isObject()) { auto ob = json.toObject(); options.provider = true; from_json(options.triggerCharacters, ob.value(QStringLiteral("triggerCharacters"))); } } static void from_json(LSPServerCapabilities & caps, const QJsonObject & json) { auto sync = json.value(QStringLiteral("textDocumentSync")); caps.textDocumentSync = (LSPDocumentSyncKind) (sync.isObject() ? sync.toObject().value(QStringLiteral("change")) : sync).toInt((int)LSPDocumentSyncKind::None); caps.hoverProvider = json.value(QStringLiteral("hoverProvider")).toBool(); from_json(caps.completionProvider, json.value(QStringLiteral("completionProvider"))); from_json(caps.signatureHelpProvider, json.value(QStringLiteral("signatureHelpProvider"))); caps.definitionProvider = json.value(QStringLiteral("definitionProvider")).toBool(); caps.declarationProvider = json.value(QStringLiteral("declarationProvider")).toBool(); caps.referencesProvider = json.value(QStringLiteral("referencesProvider")).toBool(); caps.documentSymbolProvider = json.value(QStringLiteral("documentSymbolProvider")).toBool(); caps.documentHighlightProvider = json.value(QStringLiteral("documentHighlightProvider")).toBool(); } using GenericReplyType = QJsonValue; using GenericReplyHandler = ReplyHandler; class LSPClientServer::LSPClientServerPrivate { typedef LSPClientServerPrivate self_type; LSPClientServer *q; // server cmd line QStringList m_server; // workspace root to pass along QUrl m_root; // user provided init QJsonValue m_init; // server process QProcess m_sproc; // server declared capabilites LSPServerCapabilities m_capabilities; // server state State m_state = State::None; // last msg id int m_id = 0; // receive buffer QByteArray m_receive; // registered reply handlers QHash m_handlers; public: LSPClientServerPrivate(LSPClientServer * _q, const QStringList & server, const QUrl & root, const QJsonValue & init) : q(_q), m_server(server), m_root(root), m_init(init) { // setup async reading - QObject::connect(&m_sproc, &QProcess::readyRead, mem_fun(&self_type::read, this)); - QObject::connect(&m_sproc, &QProcess::stateChanged, mem_fun(&self_type::onStateChanged, this)); + QObject::connect(&m_sproc, &QProcess::readyRead, utils::mem_fun(&self_type::read, this)); + QObject::connect(&m_sproc, &QProcess::stateChanged, utils::mem_fun(&self_type::onStateChanged, this)); } ~LSPClientServerPrivate() { stop(TIMEOUT_SHUTDOWN, TIMEOUT_SHUTDOWN); } const QStringList& cmdline() const { return m_server; } State state() { return m_state; } const LSPServerCapabilities& capabilities() { return m_capabilities; } int cancel(int reqid) { // TODO also send cancel to server m_handlers.remove(reqid); return -1; } private: void setState(State s) { if (m_state != s) { m_state = s; emit q->stateChanged(q); } } RequestHandle write(const QJsonObject & msg, const GenericReplyHandler & h = nullptr, int * id = nullptr) { RequestHandle ret; ret.m_server = q; if (!running()) return ret; auto ob = msg; ob.insert(QStringLiteral("jsonrpc"), QStringLiteral("2.0")); // notification == no handler if (h) { ob.insert(MEMBER_ID, ++m_id); ret.m_id = m_id; m_handlers[m_id] = h; } else if (id) { ob.insert(MEMBER_ID, *id); } QJsonDocument json(ob); auto sjson = json.toJson(); qCInfo(LSPCLIENT) << "calling" << msg[MEMBER_METHOD].toString(); qCDebug(LSPCLIENT) << "sending message:\n" << QString::fromUtf8(sjson); // some simple parsers expect length header first auto hdr = QStringLiteral("Content-Length: %1\r\n").arg(sjson.length()); // write is async, so no blocking wait occurs here m_sproc.write(hdr.toLatin1()); m_sproc.write("Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n"); m_sproc.write(sjson); return ret; } RequestHandle send(const QJsonObject & msg, const GenericReplyHandler & h = nullptr) { Q_ASSERT (m_state == State::Running); if (m_state == State::Running) return write(msg, h); return RequestHandle(); } void read() { // accumulate in buffer m_receive.append(m_sproc.readAllStandardOutput()); // try to get one (or more) message QByteArray &buffer = m_receive; while (true) { qCDebug(LSPCLIENT) << "buffer size" << buffer.length(); // TODO constant auto header = QByteArray("Content-Length:"); int index = buffer.indexOf(header); if (index < 0) { // avoid collecting junk if (buffer.length() > 1 << 20) buffer.clear(); break; } index += header.length(); int endindex = buffer.indexOf("\r\n", index); auto msgstart = buffer.indexOf("\r\n\r\n", index); if (endindex < 0 || msgstart < 0) break; msgstart += 4; bool ok = false; auto length = buffer.mid(index, endindex - index).toInt(&ok, 10); // FIXME perhaps detect if no reply for some time // then again possibly better left to user to restart in such case if (!ok) { qCWarning(LSPCLIENT) << "invalid Content-Length"; // flush and try to carry on to some next header buffer.remove(0, msgstart); continue; } // sanity check to avoid extensive buffering if (length > 1 << 29) { qCWarning(LSPCLIENT) << "excessive size"; buffer.clear(); continue; } if (msgstart + length > buffer.length()) break; // now onto payload auto payload = buffer.mid(msgstart, length); buffer.remove(0, msgstart + length); qCInfo(LSPCLIENT) << "got message payload size " << length; qCDebug(LSPCLIENT) << "message payload:\n" << payload; QJsonParseError error{}; auto msg = QJsonDocument::fromJson(payload, &error); if (error.error != QJsonParseError::NoError || !msg.isObject()) { qCWarning(LSPCLIENT) << "invalid response payload"; continue; } auto result = msg.object(); // check if it is the expected result int msgid = -1; if (result.contains(MEMBER_ID)) { msgid = result[MEMBER_ID].toInt(); } else { // notification; never mind those for now qCWarning(LSPCLIENT) << "discarding notification" << msg[MEMBER_METHOD].toString(); continue; } // could be request if (result.contains(MEMBER_METHOD)) { write(init_error(LSPErrorCode::MethodNotFound, result.value(MEMBER_METHOD).toString()), nullptr, &msgid); continue; } // a valid reply; what to do with it now auto it = m_handlers.find(msgid); if (it != m_handlers.end()) { (*it)(result.value(MEMBER_RESULT)); m_handlers.erase(it); } else { // could have been canceled qCDebug(LSPCLIENT) << "unexpected reply id"; } } } static QJsonObject init_error(const LSPErrorCode code, const QString & msg) { return QJsonObject { { MEMBER_ERROR, QJsonObject { { MEMBER_CODE, (int) code }, { MEMBER_MESSAGE, msg } } } }; } static QJsonObject init_request(const QString & method, const QJsonObject & params = QJsonObject()) { return QJsonObject { { MEMBER_METHOD, method }, { MEMBER_PARAMS, params } }; } bool running() { return m_sproc.state() == QProcess::Running; } void onStateChanged(QProcess::ProcessState nstate) { if (nstate == QProcess::NotRunning) { setState(State::None); } } void shutdown() { if (m_state == State::Running) { qCInfo(LSPCLIENT) << "shutting down" << m_server; // cancel all pending m_handlers.clear(); // shutdown sequence send(init_request(QStringLiteral("shutdown"))); // maybe we will get/see reply on the above, maybe not // but not important or useful either way send(init_request(QStringLiteral("exit"))); // no longer fit for regular use setState(State::Shutdown); } } void onInitializeReply(const QJsonValue & value) { // only parse parts that we use later on from_json(m_capabilities, value.toObject().value(QStringLiteral("capabilities")).toObject()); // finish init initialized(); } void initialize() { QJsonObject capabilities { { QStringLiteral("textDocument"), QJsonObject { { QStringLiteral("documentSymbol"), QJsonObject { { QStringLiteral("hierarchicalDocumentSymbolSupport"), true } } } } } }; // NOTE a typical server does not use root all that much, // other than for some corner case (in) requests QJsonObject params { { QStringLiteral("processId"), QCoreApplication::applicationPid() }, { QStringLiteral("rootPath"), m_root.path() }, { QStringLiteral("rootUri"), m_root.toString() }, { QStringLiteral("capabilities"), capabilities }, { QStringLiteral("initializationOptions"), m_init } }; // write(init_request(QStringLiteral("initialize"), params), - mem_fun(&self_type::onInitializeReply, this)); + utils::mem_fun(&self_type::onInitializeReply, this)); } void initialized() { write(init_request(QStringLiteral("initialized"))); setState(State::Running); } public: bool start() { if (m_state != State::None) return true; auto program = m_server.front(); auto args = m_server; args.pop_front(); qCInfo(LSPCLIENT) << "starting" << m_server; // at least we see some errors somewhere then m_sproc.setProcessChannelMode(QProcess::ForwardedErrorChannel); m_sproc.setReadChannel(QProcess::QProcess::StandardOutput); m_sproc.start(program, args); bool result = m_sproc.waitForStarted(); if (!result) { qCWarning(LSPCLIENT) << m_sproc.error(); } else { setState(State::Started); // perform initial handshake initialize(); } return result; } void stop(int to_term, int to_kill) { if (running()) { shutdown(); if ((to_term >= 0) && !m_sproc.waitForFinished(to_term)) m_sproc.terminate(); if ((to_kill >= 0) && !m_sproc.waitForFinished(to_kill)) m_sproc.kill(); } } RequestHandle documentSymbols(const QUrl & document, const GenericReplyHandler & h) { auto params = textDocumentParams(document); return send(init_request(QStringLiteral("textDocument/documentSymbol"), params), h); } RequestHandle documentDefinition(const QUrl & document, const LSPPosition & pos, const GenericReplyHandler & h) { auto params = textDocumentPositionParams(document, pos); return send(init_request(QStringLiteral("textDocument/definition"), params), h); } RequestHandle documentDeclaration(const QUrl & document, const LSPPosition & pos, const GenericReplyHandler & h) { auto params = textDocumentPositionParams(document, pos); return send(init_request(QStringLiteral("textDocument/declaration"), params), h); } RequestHandle documentHover(const QUrl & document, const LSPPosition & pos, const GenericReplyHandler & h) { auto params = textDocumentPositionParams(document, pos); return send(init_request(QStringLiteral("textDocument/hover"), params), h); } RequestHandle documentHighlight(const QUrl & document, const LSPPosition & pos, const GenericReplyHandler & h) { auto params = textDocumentPositionParams(document, pos); return send(init_request(QStringLiteral("textDocument/documentHighlight"), params), h); } RequestHandle documentReferences(const QUrl & document, const LSPPosition & pos, bool decl, const GenericReplyHandler & h) { auto params = referenceParams(document, pos, decl); return send(init_request(QStringLiteral("textDocument/references"), params), h); } RequestHandle documentCompletion(const QUrl & document, const LSPPosition & pos, const GenericReplyHandler & h) { auto params = textDocumentPositionParams(document, pos); return send(init_request(QStringLiteral("textDocument/completion"), params), h); } RequestHandle signatureHelp(const QUrl & document, const LSPPosition & pos, const GenericReplyHandler & h) { auto params = textDocumentPositionParams(document, pos); return send(init_request(QStringLiteral("textDocument/signatureHelp"), params), h); } void didOpen(const QUrl & document, int version, const QString & text) { auto params = textDocumentParams(textDocumentItem(document, QString(), text, version)); send(init_request(QStringLiteral("textDocument/didOpen"), params)); } void didChange(const QUrl & document, int version, const QString & text) { auto params = textDocumentParams(document, version); params[QStringLiteral("contentChanges")] = QJsonArray { QJsonObject {{MEMBER_TEXT, text}} }; send(init_request(QStringLiteral("textDocument/didChange"), params)); } void didSave(const QUrl & document, const QString & text) { auto params = textDocumentParams(document); params[QStringLiteral("text")] = text; send(init_request(QStringLiteral("textDocument/didSave"), params)); } void didClose(const QUrl & document) { auto params = textDocumentParams(document); send(init_request(QStringLiteral("textDocument/didClose"), params)); } }; // follow suit; as performed in kate docmanager // normalize at this stage/layer to avoid surprises elsewhere // sadly this is not a single QUrl method as one might hope ... static QUrl normalizeUrl(const QUrl & url) { QUrl u(url.adjusted(QUrl::NormalizePathSegments)); // Resolve symbolic links for local files (done anyway in KTextEditor) if (u.isLocalFile()) { QString normalizedUrl = QFileInfo(u.toLocalFile()).canonicalFilePath(); if (!normalizedUrl.isEmpty()) { u = QUrl::fromLocalFile(normalizedUrl); } } return u; } static LSPMarkupContent parseMarkupContent(const QJsonValue & v) { LSPMarkupContent ret; if (v.isObject()) { const auto& vm = v.toObject(); ret.value = vm.value(QStringLiteral("value")).toString(); auto kind = vm.value(MEMBER_KIND).toString(); if (kind == QStringLiteral("plaintext")) { ret.kind = LSPMarkupKind::PlainText; } else if (kind == QStringLiteral("markdown")) { ret.kind = LSPMarkupKind::MarkDown; } } else if (v.isString()) { ret.kind = LSPMarkupKind::PlainText; ret.value = v.toString(); } return ret; } static LSPPosition parsePosition(const QJsonObject & m) { auto line = m.value(MEMBER_LINE).toInt(-1); auto column = m.value(MEMBER_CHARACTER).toInt(-1); return {line, column}; } static bool isPositionValid(const LSPPosition & pos) { return pos.isValid(); } static LSPRange parseRange(const QJsonObject & range) { auto startpos = parsePosition(range.value(MEMBER_START).toObject()); auto endpos = parsePosition(range.value(MEMBER_END).toObject()); return {startpos, endpos}; } static LSPLocation parseLocation(const QJsonObject & loc) { auto uri = normalizeUrl(QUrl(loc.value(MEMBER_URI).toString())); auto range = parseRange(loc.value(MEMBER_RANGE).toObject()); return {QUrl(uri), range}; } static LSPDocumentHighlight parseDocumentHighlight(const QJsonValue & result) { auto hover = result.toObject(); auto range = parseRange(hover.value(MEMBER_RANGE).toObject()); auto kind = (LSPDocumentHighlightKind)hover.value(MEMBER_KIND).toInt((int)LSPDocumentHighlightKind::Text); // default is DocumentHighlightKind.Text return {range, kind}; } static QList parseDocumentHighlightList(const QJsonValue & result) { QList ret; // could be array if (result.isArray()) { for (const auto & def : result.toArray()) { ret.push_back(parseDocumentHighlight(def)); } } else if (result.isObject()) { // or a single value ret.push_back(parseDocumentHighlight(result)); } return ret; } static LSPHover parseHover(const QJsonValue & result) { LSPHover ret; auto hover = result.toObject(); // normalize content which can be of many forms ret.range = parseRange(hover.value(MEMBER_RANGE).toObject()); auto contents = hover.value(QStringLiteral("contents")); if (contents.isString()) { ret.contents.value = contents.toString(); } else { // should be object, pretend so auto cont = contents.toObject(); auto text = cont.value(QStringLiteral("value")).toString(); if (text.isEmpty()) { // nothing to lose, try markdown ret.contents = parseMarkupContent(contents); } else { ret.contents.value = text; } } if (ret.contents.value.length()) ret.contents.kind = LSPMarkupKind::PlainText; return ret; } static QList parseDocumentSymbols(const QJsonValue & result) { // the reply could be old SymbolInformation[] or new (hierarchical) DocumentSymbol[] // try to parse it adaptively in any case // if new style, hierarchy is specified clearly in reply // if old style, it is assumed the values enter linearly, that is; // * a parent/container is listed before its children // * if a name is defined/declared several times and then used as a parent, // then it is the last instance that is used as a parent QList ret; QMap index; std::function parseSymbol = [&] (const QJsonObject & symbol, LSPSymbolInformation *parent) { // if flat list, try to find parent by name if (!parent) { auto container = symbol.value(QStringLiteral("containerName")).toString(); parent = index.value(container, nullptr); } auto list = parent ? &parent->children : &ret; auto name = symbol.value(QStringLiteral("name")).toString(); auto kind = (LSPSymbolKind) symbol.value(MEMBER_KIND).toInt(); const auto& location = symbol.value(MEMBER_LOCATION).toObject(); const auto& mrange = symbol.contains(MEMBER_RANGE) ? symbol.value(MEMBER_RANGE) : location.value(MEMBER_RANGE); auto range = parseRange(mrange.toObject()); if (isPositionValid(range.start()) && isPositionValid(range.end())) { list->push_back({name, kind, range}); index[name] = &list->back(); // proceed recursively for (const auto &child : symbol.value(QStringLiteral("children")).toArray()) parseSymbol(child.toObject(), &list->back()); } }; for (const auto& info : result.toArray()) { parseSymbol(info.toObject(), nullptr); } return ret; } static QList parseDocumentLocation(const QJsonValue & result) { QList ret; // could be array if (result.isArray()) { for (const auto & def : result.toArray()) { ret.push_back(parseLocation(def.toObject())); } } else if (result.isObject()) { // or a single value ret.push_back(parseLocation(result.toObject())); } return ret; } static QList parseDocumentCompletion(const QJsonValue & result) { QList ret; QJsonArray items = result.toArray(); // might be CompletionList if (items.size() == 0) { items = result.toObject().value(QStringLiteral("items")).toArray(); } for (const auto & vitem : items) { const auto & item = vitem.toObject(); auto label = item.value(MEMBER_LABEL).toString(); auto detail = item.value(MEMBER_DETAIL).toString(); auto doc = parseMarkupContent(item.value(MEMBER_DOCUMENTATION)); auto sortText = item.value(QStringLiteral("sortText")).toString(); auto insertText = item.value(QStringLiteral("insertText")).toString(); auto kind = (LSPCompletionItemKind) item.value(MEMBER_KIND).toInt(); ret.push_back({label, kind, detail, doc, sortText, insertText}); } return ret; } static LSPSignatureInformation parseSignatureInformation(const QJsonObject & json) { LSPSignatureInformation info; info.label = json.value(MEMBER_LABEL).toString(); info.documentation = parseMarkupContent(json.value(MEMBER_DOCUMENTATION)); for (const auto & rpar : json.value(QStringLiteral("parameters")).toArray()) { auto par = rpar.toObject(); auto label = par.value(MEMBER_LABEL); int begin = -1, end = -1; if (label.isArray()) { auto range = label.toArray(); if (range.size() == 2) { begin = range.at(0).toInt(-1); end = range.at(1).toInt(-1); if (begin > info.label.length()) begin = -1; if (end > info.label.length()) end = -1; } } else { auto sub = label.toString(); if (sub.length()) { begin = info.label.indexOf(sub); if (begin >= 0) { end = begin + sub.length(); } } } info.parameters.push_back({begin, end}); } return info; } static LSPSignatureHelp parseSignatureHelp(const QJsonValue & result) { LSPSignatureHelp ret; QJsonObject sig = result.toObject(); for (const auto & info: sig.value(QStringLiteral("signatures")).toArray()) { ret.signatures.push_back(parseSignatureInformation(info.toObject())); } ret.activeSignature = sig.value(QStringLiteral("activeSignature")).toInt(0); ret.activeParameter = sig.value(QStringLiteral("activeParameter")).toInt(0); ret.activeSignature = qMin(qMax(ret.activeSignature, 0), ret.signatures.size()); ret.activeParameter = qMin(qMax(ret.activeParameter, 0), ret.signatures.size()); return ret; } -// prevent argument deduction -template struct identity { typedef T type; }; - // generic convert handler // sprinkle some connection-like context safety // not so likely relevant/needed due to typical sequence of events, // but in case the latter would be changed in surprising ways ... template static GenericReplyHandler make_handler(const ReplyHandler & h, const QObject *context, - typename identity>::type c) + typename utils::identity>::type c) { QPointer ctx(context); return [ctx, h, c] (const GenericReplyType & m) { if (ctx) h(c(m)); }; } LSPClientServer::LSPClientServer(const QStringList & server, const QUrl & root, const QJsonValue & init) : d(new LSPClientServerPrivate(this, server, root, init)) {} LSPClientServer::~LSPClientServer() { delete d; } const QStringList& LSPClientServer::cmdline() const { return d->cmdline(); } LSPClientServer::State LSPClientServer::state() const { return d->state(); } const LSPServerCapabilities& LSPClientServer::capabilities() const { return d->capabilities(); } bool LSPClientServer::start() { return d->start(); } void LSPClientServer::stop(int to_t, int to_k) { return d->stop(to_t, to_k); } int LSPClientServer::cancel(int reqid) { return d->cancel(reqid); } LSPClientServer::RequestHandle LSPClientServer::documentSymbols(const QUrl & document, const QObject *context, const DocumentSymbolsReplyHandler & h) { return d->documentSymbols(document, make_handler(h, context, parseDocumentSymbols)); } LSPClientServer::RequestHandle LSPClientServer::documentDefinition(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentDefinitionReplyHandler & h) { return d->documentDefinition(document, pos, make_handler(h, context, parseDocumentLocation)); } LSPClientServer::RequestHandle LSPClientServer::documentDeclaration(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentDefinitionReplyHandler & h) { return d->documentDeclaration(document, pos, make_handler(h, context, parseDocumentLocation)); } LSPClientServer::RequestHandle LSPClientServer::documentHover(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentHoverReplyHandler & h) { return d->documentHover(document, pos, make_handler(h, context, parseHover)); } LSPClientServer::RequestHandle LSPClientServer::documentHighlight(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentHighlightReplyHandler & h) { return d->documentHighlight(document, pos, make_handler(h, context, parseDocumentHighlightList)); } LSPClientServer::RequestHandle LSPClientServer::documentReferences(const QUrl & document, const LSPPosition & pos, bool decl, const QObject *context, const DocumentDefinitionReplyHandler & h) { return d->documentReferences(document, pos, decl, make_handler(h, context, parseDocumentLocation)); } LSPClientServer::RequestHandle LSPClientServer::documentCompletion(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentCompletionReplyHandler & h) { return d->documentCompletion(document, pos, make_handler(h, context, parseDocumentCompletion)); } LSPClientServer::RequestHandle LSPClientServer::signatureHelp(const QUrl & document, const LSPPosition & pos, const QObject *context, const SignatureHelpReplyHandler & h) { return d->signatureHelp(document, pos, make_handler(h, context, parseSignatureHelp)); } void LSPClientServer::didOpen(const QUrl & document, int version, const QString & text) { return d->didOpen(document, version, text); } void LSPClientServer::didChange(const QUrl & document, int version, const QString & text) { return d->didChange(document, version, text); } void LSPClientServer::didSave(const QUrl & document, const QString & text) { return d->didSave(document, text); } void LSPClientServer::didClose(const QUrl & document) { return d->didClose(document); } diff --git a/addons/lspclient/lspclientserver.h b/addons/lspclient/lspclientserver.h index 2b35c5bb9..a3b6570c3 100644 --- a/addons/lspclient/lspclientserver.h +++ b/addons/lspclient/lspclientserver.h @@ -1,143 +1,151 @@ /*************************************************************************** * Copyright (C) 2019 by Mark Nauwelaerts * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #ifndef LSPCLIENTSERVER_H #define LSPCLIENTSERVER_H #include "lspclientprotocol.h" #include #include #include #include #include #include #include #include +namespace utils +{ + // template helper // function bind helpers template inline std::function mem_fun(R (T::*pm)(Args ...), Tp object) { return [object, pm](Args... args) { return (object->*pm)(std::forward(args)...); }; } +// prevent argument deduction +template struct identity { typedef T type; }; + +} // namespace utils + template inline std::function mem_fun(R (T::*pm)(Args ...) const, Tp object) { return [object, pm](Args... args) { return (object->*pm)(std::forward(args)...); }; } static const int TIMEOUT_SHUTDOWN = 200; template using ReplyHandler = std::function; using DocumentSymbolsReplyHandler = ReplyHandler>; using DocumentDefinitionReplyHandler = ReplyHandler>; using DocumentHighlightReplyHandler = ReplyHandler>; using DocumentHoverReplyHandler = ReplyHandler; using DocumentCompletionReplyHandler = ReplyHandler>; using SignatureHelpReplyHandler = ReplyHandler; class LSPClientServer : public QObject { Q_OBJECT public: enum class State { None, Started, Running, Shutdown }; class LSPClientServerPrivate; class RequestHandle { friend class LSPClientServerPrivate; QPointer m_server; int m_id = -1; public: RequestHandle& cancel() { if (m_server) m_server->cancel(m_id); return *this; } }; LSPClientServer(const QStringList & server, const QUrl & root, const QJsonValue & init = QJsonValue()); ~LSPClientServer(); // server management // request start bool start(); // request shutdown/stop // if to_xxx >= 0 -> send signal if not exit'ed after timeout void stop(int to_term_ms, int to_kill_ms); int cancel(int id); // properties const QStringList& cmdline() const; State state() const; Q_SIGNAL void stateChanged(LSPClientServer * server); const LSPServerCapabilities& capabilities() const; // language RequestHandle documentSymbols(const QUrl & document, const QObject *context, const DocumentSymbolsReplyHandler & h); RequestHandle documentDefinition(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentDefinitionReplyHandler & h); RequestHandle documentDeclaration(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentDefinitionReplyHandler & h); RequestHandle documentHighlight(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentHighlightReplyHandler & h); RequestHandle documentHover(const QUrl & document, const LSPPosition & pos, const QObject *context, const DocumentHoverReplyHandler & h); 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); // sync void didOpen(const QUrl & document, int version, const QString & text); void didChange(const QUrl & document, int version, const QString & text); void didSave(const QUrl & document, const QString & text); void didClose(const QUrl & document); private: // pimpl data holder LSPClientServerPrivate * const d; }; #endif diff --git a/addons/lspclient/lspclientsymbolview.cpp b/addons/lspclient/lspclientsymbolview.cpp index 900eda374..56adcbba7 100644 --- a/addons/lspclient/lspclientsymbolview.cpp +++ b/addons/lspclient/lspclientsymbolview.cpp @@ -1,381 +1,381 @@ /*************************************************************************** * Copyright (C) 2014,2018 by Kåre Särs * * Copyright (C) 2019 by Mark Nauwelaerts * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "lspclientsymbolview.h" #include #include #include #include #include #include #include #include #include #include /* * Instantiates and manages the symbol outline toolview. */ class LSPClientSymbolViewImpl : public QObject, public LSPClientSymbolView { Q_OBJECT typedef LSPClientSymbolViewImpl self_type; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; QSharedPointer m_serverManager; QScopedPointer m_toolview; // parent ownership QPointer m_symbols; QScopedPointer m_popup; // icons used in tree representation QIcon m_icon_pkg; QIcon m_icon_class; QIcon m_icon_typedef; QIcon m_icon_function; QIcon m_icon_var; // initialized/updated from plugin settings // managed by context menu later on // parent ownership QAction *m_detailsOn; QAction *m_expandOn; QAction *m_treeOn; QAction *m_sortOn; // timers to delay some todo's QTimer m_refreshTimer; QTimer m_currentItemTimer; int m_oldCursorLine; // outstanding request LSPClientServer::RequestHandle m_handle; public: LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer manager) : m_plugin(plugin), m_mainWindow(mainWin), m_serverManager(manager) { m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block")); m_icon_class = QIcon::fromTheme(QStringLiteral("code-class")); m_icon_typedef = QIcon::fromTheme(QStringLiteral("code-typedef")); m_icon_function = QIcon::fromTheme(QStringLiteral("code-function")); m_icon_var = QIcon::fromTheme(QStringLiteral("code-variable")); m_toolview.reset(m_mainWindow->createToolView(plugin, QStringLiteral("lspclient_symbol_outline"), KTextEditor::MainWindow::Right, QIcon::fromTheme(QStringLiteral("code-context")), i18n("LSP Client Symbol Outline"))); QWidget *container = new QWidget(m_toolview.get()); QHBoxLayout *layout = new QHBoxLayout(container); m_symbols = new QTreeWidget(); m_symbols->setFocusPolicy(Qt::NoFocus); m_symbols->setLayoutDirection( Qt::LeftToRight ); layout->addWidget(m_symbols, 10); layout->setContentsMargins(0,0,0,0); QStringList titles; titles << i18nc("@title:column", "Symbols") << i18nc("@title:column", "Position"); m_symbols->setColumnCount(3); m_symbols->setHeaderLabels(titles); m_symbols->setColumnHidden(1, true); m_symbols->setColumnHidden(2, true); m_symbols->setContextMenuPolicy(Qt::CustomContextMenu); m_symbols->setIndentation(10); connect(m_symbols, &QTreeWidget::itemClicked, this, &self_type::goToSymbol); connect(m_symbols, &QTreeWidget::customContextMenuRequested, this, &self_type::showContextMenu); connect(m_symbols, &QTreeWidget::itemExpanded, this, &self_type::updateCurrentTreeItem); connect(m_symbols, &QTreeWidget::itemCollapsed, this, &self_type::updateCurrentTreeItem); // context menu m_popup.reset(new QMenu(m_symbols)); m_treeOn = m_popup->addAction(i18n("Tree Mode"), this, &self_type::displayOptionChanged); m_treeOn->setCheckable(true); m_expandOn = m_popup->addAction(i18n("Automatically Expand Tree"), this, &self_type::displayOptionChanged); m_expandOn->setCheckable(true); m_sortOn = m_popup->addAction(i18n("Sort Alphabetically"), this, &self_type::displayOptionChanged); m_sortOn->setCheckable(true); m_detailsOn = m_popup->addAction(i18n("Show Details"), this, &self_type::displayOptionChanged); m_detailsOn->setCheckable(true); m_popup->addSeparator(); m_popup->addAction(i18n("Expand All"), this, &self_type::expandAll); m_popup->addAction(i18n("Collapse All"), this, &self_type::collapseAll); // sync with plugin settings if updated connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated); // get updated m_refreshTimer.setSingleShot(true); connect(&m_refreshTimer, &QTimer::timeout, this, &self_type::refresh); m_currentItemTimer.setSingleShot(true); connect(&m_currentItemTimer, &QTimer::timeout, this, &self_type::updateCurrentTreeItem); connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::viewChanged); connect(m_serverManager.get(), &LSPClientServerManager::serverChanged, this, &self_type::refresh); // initial trigger configUpdated(); } void displayOptionChanged() { m_expandOn->setEnabled(m_treeOn->isChecked()); refresh(); } void configUpdated() { m_treeOn->setChecked(m_plugin->m_symbolTree); m_detailsOn->setChecked(m_plugin->m_symbolDetails); m_expandOn->setChecked(m_plugin->m_symbolExpand); m_sortOn->setChecked(m_plugin->m_symbolSort); displayOptionChanged(); } void showContextMenu(const QPoint&) { m_popup->popup(QCursor::pos(), m_treeOn); } void viewChanged(KTextEditor::View *view) { refresh(); if (view) { connect(view, &KTextEditor::View::cursorPositionChanged, this, &self_type::cursorPositionChanged, Qt::UniqueConnection); if (view->document()) { connect(view->document(), &KTextEditor::Document::textChanged, this, &self_type::textChanged, Qt::UniqueConnection); } } } void textChanged() { // refresh also updates current position m_currentItemTimer.stop(); m_refreshTimer.start(500); } void cursorPositionChanged(KTextEditor::View *view, const KTextEditor::Cursor &newPosition) { if (m_refreshTimer.isActive()) { // update will come upon refresh return; } if (view && newPosition.line() != m_oldCursorLine) { m_oldCursorLine = newPosition.line(); m_currentItemTimer.start(100); } } void makeNodes(const QList & symbols, bool tree, bool show_detail, QTreeWidget * widget, QTreeWidgetItem * parent, int * details) { QIcon *icon = nullptr; for (const auto& symbol: symbols) { switch (symbol.kind) { case LSPSymbolKind::File: case LSPSymbolKind::Module: case LSPSymbolKind::Namespace: case LSPSymbolKind::Package: if (symbol.children.count() == 0) continue; icon = &m_icon_pkg; break; case LSPSymbolKind::Class: case LSPSymbolKind::Interface: icon = &m_icon_class; break; case LSPSymbolKind::Enum: icon = &m_icon_typedef; break; case LSPSymbolKind::Method: case LSPSymbolKind::Function: case LSPSymbolKind::Constructor: icon = &m_icon_function; break; // all others considered/assumed Variable case LSPSymbolKind::Variable: case LSPSymbolKind::Constant: case LSPSymbolKind::String: case LSPSymbolKind::Number: case LSPSymbolKind::Property: case LSPSymbolKind::Field: default: // skip local variable // property, field, etc unlikely in such case anyway if (parent && parent->icon(0).cacheKey() == m_icon_function.cacheKey()) continue; icon = &m_icon_var; } auto node = parent && tree ? new QTreeWidgetItem(parent) : new QTreeWidgetItem(widget); if (!symbol.detail.isEmpty() && details) ++details; auto detail = show_detail ? symbol.detail : QStringLiteral(""); node->setText(0, symbol.name + detail); node->setIcon(0, *icon); node->setText(1, QString::number(symbol.range.start().line(), 10)); node->setText(2, QString::number(symbol.range.end().line(), 10)); // recurse children makeNodes(symbol.children, tree, show_detail, widget, node, details); } } void onDocumentSymbols(const QList & outline) { if (!m_symbols) return; // populate with sort disabled Qt::SortOrder sortOrder = m_symbols->header()->sortIndicatorOrder(); m_symbols->clear(); m_symbols->setSortingEnabled(false); int details = 0; makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), m_symbols, nullptr, &details); if (m_symbols->topLevelItemCount() == 0) { QTreeWidgetItem *node = new QTreeWidgetItem(m_symbols); node->setText(0, i18n("No outline items")); } if (m_expandOn->isChecked()) expandAll(); // disable detail setting if no such info available // (as an indication there is nothing to show anyway) if (!details) m_detailsOn->setEnabled(false); if (m_sortOn->isChecked()) { m_symbols->setSortingEnabled(true); m_symbols->sortItems(0, sortOrder); } // current item tracking updateCurrentTreeItem(); m_oldCursorLine = -1; } void refresh() { m_handle.cancel(); auto view = m_mainWindow->activeView(); auto server = m_serverManager->findServer(view); if (server) { server->documentSymbols(view->document()->url(), this, - mem_fun(&self_type::onDocumentSymbols, this)); + utils::mem_fun(&self_type::onDocumentSymbols, this)); } else if (m_symbols) { m_symbols->clear(); QTreeWidgetItem *node = new QTreeWidgetItem(m_symbols); node->setText(0, i18n("No server available")); } } QTreeWidgetItem* getCurrentItem(QTreeWidgetItem * item, int line) { for (int i = 0; i < item->childCount(); i++) { auto citem = getCurrentItem(item->child(i), line); if (citem) return citem; } int lstart = item->data(1, Qt::DisplayRole).toInt(); int lend = item->data(2, Qt::DisplayRole).toInt(); if (lstart <= line && line <= lend) return item; return nullptr; } void updateCurrentTreeItem() { KTextEditor::View* editView = m_mainWindow->activeView(); if (!editView || !m_symbols) { return; } int currLine = editView->cursorPositionVirtual().line(); auto item = getCurrentItem(m_symbols->invisibleRootItem(), currLine); // go up until a non-expanded item is found // (the others were collapsed for some reason ...) while (item) { auto parent = item->parent(); if (parent && !parent->isExpanded()) { item = parent; } else { break; } } m_symbols->blockSignals(true); m_symbols->setCurrentItem(item); m_symbols->blockSignals(false); } void expandAll() { if (!m_symbols) return; QTreeWidgetItemIterator it(m_symbols, QTreeWidgetItemIterator::HasChildren); while (*it) { m_symbols->expandItem(*it); ++it; } } void collapseAll() { if (!m_symbols) return; QTreeWidgetItemIterator it(m_symbols, QTreeWidgetItemIterator::HasChildren); while (*it) { m_symbols->collapseItem(*it); ++it; } } void goToSymbol(QTreeWidgetItem *it) { KTextEditor::View *kv = m_mainWindow->activeView(); if (kv && it && !it->text(1).isEmpty()) { kv->setCursorPosition(KTextEditor::Cursor(it->text(1).toInt(nullptr, 10), 0)); } } }; QObject* LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer manager) { return new LSPClientSymbolViewImpl(plugin, mainWin, manager); } #include "lspclientsymbolview.moc"