diff --git a/addons/lspclient/lspclientprotocol.h b/addons/lspclient/lspclientprotocol.h index 72a5bd2d7..12f613f29 100644 --- a/addons/lspclient/lspclientprotocol.h +++ b/addons/lspclient/lspclientprotocol.h @@ -1,230 +1,267 @@ /*************************************************************************** * 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 LSPCLIENTPROTOCOL_H #define LSPCLIENTPROTOCOL_H #include #include #include #include #include #include +// Following types roughly follow the types/interfaces as defined in LSP protocol spec +// although some deviation may arise where it has been deemed useful +// Moreover, to avoid introducing a custom 'optional' type, absence of an optional +// part/member is usually signalled by some 'invalid' marker (empty, negative). + enum class LSPErrorCode { // Defined by JSON RPC ParseError = -32700, InvalidRequest = -32600, MethodNotFound = -32601, InvalidParams = -32602, InternalError = -32603, serverErrorStart = -32099, serverErrorEnd = -32000, ServerNotInitialized = -32002, UnknownErrorCode = -32001, // Defined by the protocol. RequestCancelled = -32800, ContentModified = -32801 }; enum class LSPDocumentSyncKind { None = 0, Full = 1, Incremental = 2 }; struct LSPCompletionOptions { bool provider = false; bool resolveProvider = false; QVector triggerCharacters; }; struct LSPSignatureHelpOptions { bool provider = false; QVector triggerCharacters; }; struct LSPServerCapabilities { LSPDocumentSyncKind textDocumentSync = LSPDocumentSyncKind::None; bool hoverProvider = false; LSPCompletionOptions completionProvider; LSPSignatureHelpOptions signatureHelpProvider; bool definitionProvider = false; // FIXME ? clangd unofficial extension bool declarationProvider = false; bool referencesProvider = false; bool documentSymbolProvider = false; bool documentHighlightProvider = false; }; enum class LSPMarkupKind { None = 0, PlainText = 1, MarkDown = 2 }; struct LSPMarkupContent { LSPMarkupKind kind = LSPMarkupKind::None; QString value; }; /** * Language Server Protocol Position * line + column, 0 based, negative for invalid * maps 1:1 to KTextEditor::Cursor */ using LSPPosition = KTextEditor::Cursor; /** * Language Server Protocol Range * start + end tuple of LSPPosition * maps 1:1 to KTextEditor::Range */ using LSPRange = KTextEditor::Range; struct LSPLocation { QUrl uri; LSPRange range; }; enum class LSPDocumentHighlightKind { Text = 1, Read = 2, Write = 3 }; struct LSPDocumentHighlight { LSPRange range; LSPDocumentHighlightKind kind; }; struct LSPHover { LSPMarkupContent contents; LSPRange range; }; enum class LSPSymbolKind { File = 1, Module = 2, Namespace = 3, Package = 4, Class = 5, Method = 6, Property = 7, Field = 8, Constructor = 9, Enum = 10, Interface = 11, Function = 12, Variable = 13, Constant = 14, String = 15, Number = 16, Boolean = 17, Array = 18, }; struct LSPSymbolInformation { LSPSymbolInformation(const QString & _name, LSPSymbolKind _kind, LSPRange _range, const QString & _detail = QString()) : name(_name), detail(_detail), kind(_kind), range(_range) {} QString name; QString detail; LSPSymbolKind kind; LSPRange range; QList children; }; enum class LSPCompletionItemKind { Text = 1, Method = 2, Function = 3, Constructor = 4, Field = 5, Variable = 6, Class = 7, Interface = 8, Module = 9, Property = 10, Unit = 11, Value = 12, Enum = 13, Keyword = 14, Snippet = 15, Color = 16, File = 17, Reference = 18, Folder = 19, EnumMember = 20, Constant = 21, Struct = 22, Event = 23, Operator = 24, TypeParameter = 25, }; struct LSPCompletionItem { QString label; LSPCompletionItemKind kind; QString detail; LSPMarkupContent documentation; QString sortText; QString insertText; }; struct LSPParameterInformation { // offsets into overall signature label // (-1 if invalid) int start; int end; }; struct LSPSignatureInformation { QString label; LSPMarkupContent documentation; QList parameters; }; struct LSPSignatureHelp { QList signatures; int activeSignature; int activeParameter; }; +enum class LSPDiagnosticSeverity +{ + Unknown = 0, + Error = 1, + Warning = 2, + Information = 3, + Hint = 4, +}; + +struct LSPDiagnosticRelatedInformation +{ + // empty url / invalid range when absent + LSPLocation location; + QString message; +}; + +struct LSPDiagnostics +{ + LSPRange range; + LSPDiagnosticSeverity severity; + QString code; + QString source; + QString message; + LSPDiagnosticRelatedInformation relatedInformation; +}; + +struct LSPPublishDiagnosticsParams +{ + QUrl uri; + QList diagnostics; +}; + #endif diff --git a/addons/lspclient/lspclientserver.cpp b/addons/lspclient/lspclientserver.cpp index 96a241279..d018f80a3 100644 --- a/addons/lspclient/lspclientserver.cpp +++ b/addons/lspclient/lspclientserver.cpp @@ -1,912 +1,948 @@ /*************************************************************************** * 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(); } +// TODO move all parsing here +static LSPPublishDiagnosticsParams +parseDiagnostics(const QJsonObject & result); + 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, 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("\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(); + processNotification(result); 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 } } + QJsonObject { { QStringLiteral("hierarchicalDocumentSymbolSupport"), true } }, + }, + { QStringLiteral("publishDiagnostics"), + QJsonObject { { QStringLiteral("relatedInformation"), 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), 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)); } + + void processNotification(const QJsonObject & msg) + { + auto method = msg[MEMBER_METHOD].toString(); + if (method == QStringLiteral("textDocument/publishDiagnostics")) { + emit q->publishDiagnostics(parseDiagnostics(msg[MEMBER_PARAMS].toObject())); + } else { + qCWarning(LSPCLIENT) << "discarding notification" << method; + } + } }; // 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(); if (sortText.isEmpty()) sortText = label; auto insertText = item.value(QStringLiteral("insertText")).toString(); if (insertText.isEmpty()) insertText = label; 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; } +static LSPPublishDiagnosticsParams +parseDiagnostics(const QJsonObject & result) +{ + LSPPublishDiagnosticsParams ret; + + ret.uri = normalizeUrl(QUrl(result.value(MEMBER_URI).toString())); + for (const auto & vdiag : result.value(QStringLiteral("diagnostics")).toArray()) { + auto diag = vdiag.toObject(); + auto range = parseRange(diag.value(MEMBER_RANGE).toObject()); + auto severity = (LSPDiagnosticSeverity) diag.value(QStringLiteral("severity")).toInt(); + auto code = diag.value(QStringLiteral("code")).toString(); + auto source = diag.value(QStringLiteral("source")).toString(); + auto message = diag.value(MEMBER_MESSAGE).toString(); + auto related = diag.value(QStringLiteral("relatedInformation")).toObject(); + auto relLocation = parseLocation(related.value(MEMBER_LOCATION).toObject()); + auto relMessage = related.value(MEMBER_MESSAGE).toString(); + ret.diagnostics.push_back({range, severity, code, source, message, relLocation, relMessage}); + } + return ret; +} + // 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 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 d7695f3b5..bcedc2847 100644 --- a/addons/lspclient/lspclientserver.h +++ b/addons/lspclient/lspclientserver.h @@ -1,152 +1,156 @@ /*************************************************************************** * 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)...); }; } template inline std::function mem_fun(R (T::*pm)(Args ...) const, Tp object) { return [object, pm](Args... args) { return (object->*pm)(std::forward(args)...); }; } // prevent argument deduction template struct identity { typedef T type; }; } // namespace utils 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); + // notifcation = signal +Q_SIGNALS: + void publishDiagnostics(const LSPPublishDiagnosticsParams & ); + private: // pimpl data holder LSPClientServerPrivate * const d; }; #endif diff --git a/addons/lspclient/tests/lsptestapp.cpp b/addons/lspclient/tests/lsptestapp.cpp index 3cc02ad17..9bfadedf7 100644 --- a/addons/lspclient/tests/lsptestapp.cpp +++ b/addons/lspclient/tests/lsptestapp.cpp @@ -1,111 +1,117 @@ /* This file is part of the KDE project * * Copyright (C) 2019 Mark Nauwelaerts * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "../lspclientserver.h" #include #include #include #include #include int main(int argc, char ** argv) { if (argc < 5) return -1; LSPClientServer lsp(QString::fromLatin1(argv[1]).split(QStringLiteral(" ")), QUrl(QString::fromLatin1(argv[2]))); QCoreApplication app(argc, argv); QEventLoop q; auto state_h = [&lsp, &q] () { if (lsp.state() == LSPClientServer::State::Running) q.quit(); }; auto conn = QObject::connect(&lsp, &LSPClientServer::stateChanged, state_h); lsp.start(); q.exec(); QObject::disconnect(conn); + auto diagnostics_h = [] (const LSPPublishDiagnosticsParams & diag) { + std::cout << "diagnostics " << diag.uri.path().toUtf8().toStdString() << " count: " << diag.diagnostics.length(); + }; + + QObject::connect(&lsp, &LSPClientServer::publishDiagnostics, diagnostics_h); + auto document = QUrl(QString::fromLatin1(argv[3])); QFile file(document.path()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return -1; QTextStream in(&file); QString content = in.readAll(); lsp.didOpen(document, 0, content); auto ds_h = [&q] (const QList & syms) { std::cout << "symbol count: " << syms.length() << std::endl; q.quit(); }; lsp.documentSymbols(document, &app, ds_h); q.exec(); auto position = QString::fromLatin1(argv[4]).split(QStringLiteral(" ")); auto def_h = [&q] (const QList & defs) { std::cout << "definition count: " << defs.length() << std::endl; q.quit(); }; lsp.documentDefinition(document, {position[0].toInt(), position[1].toInt()}, &app, def_h); q.exec(); auto comp_h = [&q] (const QList & completions) { std::cout << "completion count: " << completions.length() << std::endl; q.quit(); }; lsp.documentCompletion(document, {position[0].toInt(), position[1].toInt()}, &app, comp_h); q.exec(); auto sig_h = [&q] (const LSPSignatureHelp & help) { std::cout << "signature help count: " << help.signatures.length() << std::endl; q.quit(); }; lsp.signatureHelp(document, {position[0].toInt(), position[1].toInt()}, &app, sig_h); q.exec(); auto hover_h = [&q] (const LSPHover & hover) { std::cout << "hover: " << hover.contents.value.toStdString() << std::endl; q.quit(); }; lsp.documentHover(document, {position[0].toInt(), position[1].toInt()}, &app, hover_h); q.exec(); auto ref_h = [&q] (const QList & refs) { std::cout << "refs: " << refs.length() << std::endl; q.quit(); }; lsp.documentReferences(document, {position[0].toInt(), position[1].toInt()}, true, &app, ref_h); q.exec(); auto hl_h = [&q] (const QList & hls) { std::cout << "highlights: " << hls.length() << std::endl; q.quit(); }; lsp.documentHighlight(document, {position[0].toInt(), position[1].toInt()}, &app, hl_h); q.exec(); // lsp.didOpen(document, 0, QStringLiteral("blah")); lsp.didChange(document, 1, QStringLiteral("foo")); lsp.didClose(document); }