diff options
-rw-r--r-- | src/app/qbs/lspserver.cpp | 223 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/lsp.qbs | 5 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/subdir/file1.txt | 0 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/subdir/file2.txt | 0 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/toplevel.txt | 0 | ||||
-rw-r--r-- | tests/auto/blackbox/tst_blackbox.cpp | 9 |
6 files changed, 200 insertions, 37 deletions
diff --git a/src/app/qbs/lspserver.cpp b/src/app/qbs/lspserver.cpp index c6cce6706..233f84da6 100644 --- a/src/app/qbs/lspserver.cpp +++ b/src/app/qbs/lspserver.cpp @@ -55,16 +55,21 @@ #include <tools/stlutils.h> #include <QBuffer> +#include <QDir> +#include <QFileInfo> #include <QLocalServer> #include <QLocalSocket> #include <QMap> +#include <functional> +#include <optional> #include <unordered_map> #ifdef Q_OS_WINDOWS #include <process.h> #else #include <unistd.h> #endif +#include <utility> namespace qbs::Internal { @@ -159,6 +164,9 @@ public: void handleDidSaveNotification(); void handleDidCloseNotification(); + bool handleGotoDefViaCodeLinks( + const QString &sourceFile, const Document *sourceDoc, const CodePosition &sourcePos); + QLocalServer server; QBuffer incomingData; lsp::BaseMessage currentMessage; @@ -349,49 +357,143 @@ void LspServer::Private::handleGotoDefinitionRequest() { const lsp::TextDocumentPositionParams params(messageObject.value(lsp::paramsKey)); const QString sourceFile = params.textDocument().uri().toLocalFile(); + const CodePosition sourcePos = posFromLspPos(params.position()); const Document *sourceDoc = nullptr; if (const auto it = documents.find(sourceFile); it != documents.end()) sourceDoc = &it->second; - const auto fileEntry = codeLinks.constFind(sourceFile); - if (fileEntry == codeLinks.constEnd()) + + // First check the codeLinks, where we map locations of Depends items + // to the location of the corresponding product or module. + // For a cursor such as this: + // Depends { name: "cpp" } + // ^ + // We return the location of the module file that implements the cpp + // backend for that particular product, e.g. GenericGCC.qbs. + if (handleGotoDefViaCodeLinks(sourceFile, sourceDoc, sourcePos)) + return; + + if (!sourceDoc) return sendResponse(nullptr); - const CodePosition sourcePos = posFromLspPos(params.position()); - if (sourceDoc && !sourceDoc->isPositionUpToDate(sourcePos)) + + // Now check for elements of the "files" property. + // For a cursor such as this: + // Group { prefix: "sources/"; files: "main.cpp" } + // ^ + // We return the absolute path of the file, including the + // group prefix. + // Note that if we recorded the location of all source artifacts, + // we would not need to do any parsing here and also we wouldn't + // be limited to string literals. But that's probably not worth + // the additional overhead. + + const int offset = posToOffset(params.position(), sourceDoc->currentContent) - 1; + if (offset < 0 || offset >= sourceDoc->currentContent.length()) return sendResponse(nullptr); - for (auto it = fileEntry->cbegin(); it != fileEntry->cend(); ++it) { - if (!it.key().contains(sourcePos)) - continue; - if (sourceDoc && !sourceDoc->isPositionUpToDate(it.key().end())) - return sendResponse(nullptr); - QList<CodeLocation> targets = it.value(); - QBS_ASSERT(!targets.isEmpty(), return sendResponse(nullptr)); - for (auto it = targets.begin(); it != targets.end();) { - const Document *targetDoc = nullptr; - if (it->filePath() == sourceFile) - targetDoc = sourceDoc; - else if (const auto docIt = documents.find(it->filePath()); docIt != documents.end()) - targetDoc = &docIt->second; - if (targetDoc && !targetDoc->isPositionUpToDate(CodePosition(it->line(), it->column()))) - it = targets.erase(it); - else - ++it; + + using namespace QbsQmlJS; + using namespace QbsQmlJS::AST; + Engine engine; + Lexer lexer(&engine); + lexer.setCode(sourceDoc->currentContent, 1); + Parser parser(&engine); + parser.parse(); + if (!parser.ast()) + return sendResponse(nullptr); + + AstNodeLocator locator(offset, *parser.ast()); + const QList<Node *> &astPath = locator.path(); + + // The AST path looks as follows: + // ... -> UiObjectDefinition -> UiObjectInitializer -> ObjectMemberList + // -> UiScriptBinding -> ExpressionStatement [-> ArrayLiteral] + // If the project file employs the syntactical sugar of using a single + // string for one-element arrays, then there is no ArrayLiteral; instead, + // the ExpressionStatement's expression is a StringLiteral that contains + // the file path. + // Sometimes, the AST path also ends in an ElementList instead of an ArrayLiteral. + // This discrepancy is probably a bug in the path() function. + + const StringLiteral *filePathLiteral = nullptr; + const ElementList *elementList = nullptr; + int filesNodeIndex = 0; + int groupNodeIndex = 0; + const Node * const filePathNode = astPath.last(); + if (filePathNode->kind == Node::Kind_ArrayLiteral) { + filesNodeIndex = astPath.size() - 3; + groupNodeIndex = astPath.size() - 6; + elementList = static_cast<const ArrayLiteral *>(filePathNode)->elements; + } else if (filePathNode->kind == Node::Kind_ElementList) { + filesNodeIndex = astPath.size() - 4; + groupNodeIndex = astPath.size() - 7; + elementList = static_cast<const ElementList *>(filePathNode); + } else if (filePathNode->kind == Node::Kind_ExpressionStatement) { + filesNodeIndex = astPath.size() - 2; + groupNodeIndex = astPath.size() - 5; + const auto filePathExpr + = static_cast<const ExpressionStatement *>(filePathNode)->expression; + if (filePathExpr && filePathExpr->kind == Node::Kind_StringLiteral) + filePathLiteral = static_cast<StringLiteral *>(filePathExpr); + } + if (elementList) { + for (const ElementList *l = elementList; l; l = l->next) { + if (l->expression->kind != Node::Kind_StringLiteral) + continue; + const auto s = static_cast<StringLiteral *>(l->expression); + if (int(s->firstSourceLocation().offset) > offset) + break; + if (int(s->lastSourceLocation().offset + s->lastSourceLocation().length) > offset) { + filePathLiteral = s; + break; + } } - struct JsonArray : public QJsonArray { void reserve(std::size_t) {}}; - const auto locations = transformed<JsonArray>(targets, - [](const CodeLocation &loc) { - const lsp::Position startPos = lspPosFromCodeLocation(loc); - const lsp::Position endPos(startPos.line(), startPos.character() + 1); - lsp::Location targetLocation; - targetLocation.setUri(lsp::DocumentUri::fromProtocol( - QUrl::fromLocalFile(loc.filePath()).toString())); - targetLocation.setRange({startPos, endPos}); - return QJsonObject(targetLocation); - }); - if (locations.size() == 1) - return sendResponse(locations.first().toObject()); - return sendResponse(locations); } - sendResponse(nullptr); + if (!filePathLiteral) + return sendResponse(nullptr); + if (groupNodeIndex < 0) + return sendResponse(nullptr); + const auto filesNode = astPath.at(filesNodeIndex); + if (filesNode->kind != Node::Kind_UiScriptBinding + || static_cast<UiScriptBinding *>(filesNode)->qualifiedId->name != "files") { + return sendResponse(nullptr); + } + const Node * const groupNode = astPath.at(groupNodeIndex); + if (groupNode->kind != Node::Kind_UiObjectDefinition) + return sendResponse(nullptr); + const SourceLocation loc = groupNode->firstSourceLocation(); + using GroupAndProduct = std::optional<std::pair<GroupData, ProductData>>; + const std::function<GroupAndProduct(const ProjectData &)> findGroup = + [&](const ProjectData &project) -> GroupAndProduct { + for (const ProductData &product : project.products()) { + for (const GroupData &group : product.groups()) { + if (group.location().filePath() == sourceFile + && group.location().line() == int(loc.startLine) + && group.location().column() == int(loc.startColumn)) + return std::make_pair(group, product); + } + } + for (const ProjectData &subProject : project.subProjects()) { + if (const auto &g = findGroup(subProject)) + return g; + } + return {}; + }; + const auto groupAndProduct = findGroup(projectData); + if (!groupAndProduct) + return sendResponse(nullptr); + QString absoluteFilePath = filePathLiteral->value.toString(); + absoluteFilePath.prepend(groupAndProduct->first.prefix()); + if (QFileInfo(absoluteFilePath).isRelative()) { + absoluteFilePath = QFileInfo(groupAndProduct->second.location().filePath()) + .absoluteDir() + .filePath(absoluteFilePath); + } + const lsp::Position startPos(0, 0); + const lsp::Position endPos(0, 1); + lsp::Location targetLocation; + targetLocation.setUri( + lsp::DocumentUri::fromProtocol(QUrl::fromLocalFile(absoluteFilePath).toString())); + targetLocation.setRange({lsp::Position(0, 0), lsp::Position(0, 1)}); + sendResponse(QJsonObject(targetLocation)); } // We operate under the assumption that the client has basic QML support. @@ -599,6 +701,54 @@ void LspServer::Private::handleDidCloseNotification() documents.erase(docIt); } +bool LspServer::Private::handleGotoDefViaCodeLinks( + const QString &sourceFile, const Document *sourceDoc, const CodePosition &sourcePos) +{ + if (sourceDoc && !sourceDoc->isPositionUpToDate(sourcePos)) + return false; + const auto fileEntry = codeLinks.constFind(sourceFile); + if (fileEntry == codeLinks.constEnd()) + return false; + for (auto it = fileEntry->begin(); it != fileEntry->cend(); ++it) { + if (!it.key().contains(sourcePos)) + continue; + if (sourceDoc && !sourceDoc->isPositionUpToDate(it.key().end())) + return false; + QList<CodeLocation> targets = it.value(); + QBS_ASSERT(!targets.isEmpty(), return false); + for (auto it = targets.begin(); it != targets.end();) { + const Document *targetDoc = nullptr; + if (it->filePath() == sourceFile) + targetDoc = sourceDoc; + else if (const auto docIt = documents.find(it->filePath()); docIt != documents.end()) + targetDoc = &docIt->second; + if (targetDoc && !targetDoc->isPositionUpToDate(CodePosition(it->line(), it->column()))) + it = targets.erase(it); + else + ++it; + } + struct JsonArray : public QJsonArray + { + void reserve(std::size_t) {} + }; + const auto locations = transformed<JsonArray>(targets, [](const CodeLocation &loc) { + const lsp::Position startPos = lspPosFromCodeLocation(loc); + const lsp::Position endPos(startPos.line(), startPos.character() + 1); + lsp::Location targetLocation; + targetLocation.setUri( + lsp::DocumentUri::fromProtocol(QUrl::fromLocalFile(loc.filePath()).toString())); + targetLocation.setRange({startPos, endPos}); + return QJsonObject(targetLocation); + }); + if (locations.size() == 1) + sendResponse(locations.first().toObject()); + else + sendResponse(locations); + return true; + } + return false; +} + static int posToOffset(const CodePosition &pos, const QString &doc) { int offset = 0; @@ -626,4 +776,3 @@ bool Document::isPositionUpToDate(const lsp::Position &pos) const } } // namespace qbs::Internal - diff --git a/tests/auto/blackbox/testdata/lsp/lsp.qbs b/tests/auto/blackbox/testdata/lsp/lsp.qbs index 24479e0ec..307147fe4 100644 --- a/tests/auto/blackbox/testdata/lsp/lsp.qbs +++ b/tests/auto/blackbox/testdata/lsp/lsp.qbs @@ -7,5 +7,10 @@ Project { } Product { Depends { name: "dep" } + files: "toplevel.txt" + Group { + prefix: "subdir/" + Group { files: ["file1.txt", "file2.txt"] } + } } } diff --git a/tests/auto/blackbox/testdata/lsp/subdir/file1.txt b/tests/auto/blackbox/testdata/lsp/subdir/file1.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/subdir/file1.txt diff --git a/tests/auto/blackbox/testdata/lsp/subdir/file2.txt b/tests/auto/blackbox/testdata/lsp/subdir/file2.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/subdir/file2.txt diff --git a/tests/auto/blackbox/testdata/lsp/toplevel.txt b/tests/auto/blackbox/testdata/lsp/toplevel.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/toplevel.txt diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp index 98fbcd7d4..6479e05a8 100644 --- a/tests/auto/blackbox/tst_blackbox.cpp +++ b/tests/auto/blackbox/tst_blackbox.cpp @@ -7002,6 +7002,15 @@ void TestBlackbox::qbsLanguageServer_data() QTest::addRow("follow to module, invalidating insert") << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:4:9") << QString() << QString("property bool dummy\n") << QString(); + QTest::addRow("follow to file in product") + << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:10:25") << QString() << QString() + << (testDataDir + "/lsp/toplevel.txt:1:1"); + QTest::addRow("follow to file in group (first element)") + << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:13:34") << QString() << QString() + << (testDataDir + "/lsp/subdir/file1.txt:1:1"); + QTest::addRow("follow to file in group (last element)") + << "--goto-def" << (testDataDir + "/lsp/lsp.qbs:13:48") << QString() << QString() + << (testDataDir + "/lsp/subdir/file2.txt:1:1"); QTest::addRow("completion: LHS, module prefix") << "--completion" << (testDataDir + "/lsp/lsp.qbs:7:1") << QString() << QString("P") << QString("Prefix.m1\nPrefix.m2\nPrefix.m3"); |