File indexing completed on 2024-05-12 04:39:08

0001 /*
0002     SPDX-FileCopyrightText: 2014 Sergey Kalinichev <kalinichev.so.0@gmail.com>
0003     SPDX-FileCopyrightText: 2015 Milian Wolff <mail@milianw.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "includepathcompletioncontext.h"
0009 
0010 #include "duchain/navigationwidget.h"
0011 #include "duchain/clanghelpers.h"
0012 
0013 #include <language/codecompletion/abstractincludefilecompletionitem.h>
0014 
0015 #include <QDirIterator>
0016 
0017 #include <KTextEditor/View>
0018 
0019 #include <algorithm>
0020 
0021 using namespace KDevelop;
0022 
0023 /**
0024  * Parse the last line of @p text and extract information about any existing include path from it.
0025  */
0026 IncludePathProperties IncludePathProperties::parseText(const QString& text, int rightBoundary)
0027 {
0028     IncludePathProperties properties;
0029 
0030     int idx = text.lastIndexOf(QLatin1Char('\n'));
0031     if (idx == -1) {
0032         idx = 0;
0033     }
0034     if (rightBoundary == -1) {
0035         rightBoundary = text.length();
0036     }
0037 
0038     // what follows is a relatively simple parser for include lines that may contain comments, i.e.:
0039     // /*comment*/ #include /*comment*/ "path.h" /*comment*/
0040     enum FindState {
0041         FindBang,
0042         FindInclude,
0043         FindType,
0044         FindTypeEnd
0045     };
0046     FindState state = FindBang;
0047     QChar expectedEnd = QLatin1Char('>');
0048     for (; idx < text.size(); ++idx) {
0049         const auto c = text.at(idx);
0050         if (c.isSpace()) {
0051             continue;
0052         }
0053         if (c == QLatin1Char('/') && state != FindTypeEnd) {
0054             // skip comments
0055             if (idx >= text.length() - 1 || text.at(idx + 1) != QLatin1Char('*')) {
0056                 properties.valid = false;
0057                 return properties;
0058             }
0059             idx += 2;
0060             while (idx < text.length() - 1 && (text.at(idx) != QLatin1Char('*') || text.at(idx + 1) != QLatin1Char('/'))) {
0061                 ++idx;
0062             }
0063             if (idx >= text.length() - 1 || text.at(idx) != QLatin1Char('*') || text.at(idx + 1) != QLatin1Char('/')) {
0064                 properties.valid = false;
0065                 return properties;
0066             }
0067             ++idx;
0068             continue;
0069         }
0070         switch (state) {
0071             case FindBang:
0072                 if (c != QLatin1Char('#')) {
0073                     return properties;
0074                 }
0075                 state = FindInclude;
0076                 break;
0077             case FindInclude:
0078                 if (text.midRef(idx, 7) != QLatin1String("include")) {
0079                     return properties;
0080                 }
0081                 idx += 6;
0082                 state = FindType;
0083                 properties.valid = true;
0084                 break;
0085             case FindType:
0086                 properties.inputFrom = idx + 1;
0087                 if (c == QLatin1Char('"')) {
0088                     expectedEnd = QLatin1Char('"');
0089                     properties.local = true;
0090                 } else if (c != QLatin1Char('<')) {
0091                     properties.valid = false;
0092                     return properties;
0093                 }
0094                 state = FindTypeEnd;
0095                 break;
0096             case FindTypeEnd:
0097                 if (c == expectedEnd) {
0098                     properties.inputTo = idx;
0099                     // stop iteration
0100                     idx = text.size();
0101                 }
0102                 break;
0103         }
0104     }
0105 
0106     if (!properties.valid) {
0107         return properties;
0108     }
0109 
0110     // properly append to existing paths without overriding it
0111     // i.e.: #include <foo/> should become #include <foo/bar.h>
0112     // or: #include <header.h> should again become #include <header.h>
0113     // see unit tests for more examples
0114     if (properties.inputFrom != -1) {
0115         int end = properties.inputTo;
0116         if (end >= rightBoundary || end == -1) {
0117             end = text.lastIndexOf(QLatin1Char('/'), rightBoundary - 1) + 1;
0118         }
0119         if (end > 0) {
0120             properties.prefixPath = text.mid(properties.inputFrom, end - properties.inputFrom);
0121             properties.inputFrom += properties.prefixPath.length();
0122         }
0123     }
0124 
0125     return properties;
0126 }
0127 
0128 namespace
0129 {
0130 
0131 QVector<KDevelop::IncludeItem> includeItemsForUrl(const QUrl& url, const IncludePathProperties& properties,
0132                                                   const ClangParsingEnvironment::IncludePaths& includePaths)
0133 {
0134     QVector<IncludeItem> includeItems;
0135     Path::List paths;
0136 
0137     if (properties.local) {
0138         paths.reserve(1 + includePaths.project.size() + includePaths.system.size());
0139         paths.push_back(Path(url).parent());
0140         paths += includePaths.project;
0141         paths += includePaths.system;
0142     } else {
0143         paths = includePaths.system + includePaths.project;
0144     }
0145 
0146     // ensure we don't add duplicate paths
0147     QSet<Path> handledPaths; // search paths
0148     QSet<QString> foundIncludePaths; // found items
0149 
0150     int pathNumber = 0;
0151     for (auto searchPath : qAsConst(paths)) {
0152         if (handledPaths.contains(searchPath)) {
0153             continue;
0154         }
0155         handledPaths.insert(searchPath);
0156 
0157         if (!properties.prefixPath.isEmpty()) {
0158             searchPath.addPath(properties.prefixPath);
0159         }
0160 
0161         QDirIterator dirIterator(searchPath.toLocalFile());
0162         while (dirIterator.hasNext()) {
0163             dirIterator.next();
0164             KDevelop::IncludeItem item;
0165             item.name = dirIterator.fileName();
0166 
0167             if (item.name.startsWith(QLatin1Char('.')) || item.name.endsWith(QLatin1Char('~'))) { //filter out ".", "..", hidden files, and backups
0168                 continue;
0169             }
0170 
0171             const auto info = dirIterator.fileInfo();
0172             item.isDirectory = info.isDir();
0173 
0174             // filter files that are not a header
0175             // note: system headers sometimes don't have any extension, and we still want to show those
0176             if (!item.isDirectory && item.name.contains(QLatin1Char('.')) && !ClangHelpers::isHeader(item.name)) {
0177                 continue;
0178             }
0179 
0180             const QString fullPath = info.canonicalFilePath();
0181             if (foundIncludePaths.contains(fullPath)) {
0182                 continue;
0183             } else {
0184                 foundIncludePaths.insert(fullPath);
0185             }
0186 
0187             item.basePath = searchPath.toUrl();
0188             item.pathNumber = pathNumber;
0189 
0190             includeItems << item;
0191         }
0192         ++pathNumber;
0193     }
0194 
0195     return includeItems;
0196 }
0197 }
0198 
0199 class IncludeFileCompletionItem : public AbstractIncludeFileCompletionItem<ClangNavigationWidget>
0200 {
0201 public:
0202     explicit IncludeFileCompletionItem(const IncludeItem& include)
0203         : AbstractIncludeFileCompletionItem<ClangNavigationWidget>(include)
0204     {}
0205 
0206     void execute(KTextEditor::View* view, const KTextEditor::Range& word) override
0207     {
0208         auto document = view->document();
0209         auto range = word;
0210         const int lineNumber = word.end().line();
0211         const QString line = document->line(lineNumber);
0212         const auto properties = IncludePathProperties::parseText(line, word.end().column());
0213         if (!properties.valid) {
0214             return;
0215         }
0216 
0217         QString newText = includeItem.isDirectory ? (includeItem.name + QLatin1Char('/')) : includeItem.name;
0218 
0219         if (properties.inputFrom == -1) {
0220             newText.prepend(QLatin1Char('<'));
0221         } else {
0222             range.setStart({lineNumber, properties.inputFrom});
0223         }
0224         if (properties.inputTo == -1) {
0225             // Add suffix
0226             if (properties.local) {
0227                 newText += QLatin1Char('"');
0228             } else {
0229                 newText += QLatin1Char('>');
0230             }
0231 
0232             // replace the whole line
0233             range.setEnd({lineNumber, line.size()});
0234         } else {
0235             range.setEnd({lineNumber, properties.inputTo});
0236         }
0237 
0238         document->replaceText(range, newText);
0239 
0240         if (includeItem.isDirectory) {
0241             // ensure we can continue to add files/paths when we just added a directory
0242             int offset = (properties.inputTo == -1) ? 1 : 0;
0243             view->setCursorPosition(range.start() + KTextEditor::Cursor(0, newText.length() - offset));
0244         } else {
0245             // place cursor at end of line
0246             view->setCursorPosition({lineNumber, document->lineLength(lineNumber)});
0247         }
0248     }
0249 };
0250 
0251 IncludePathCompletionContext::IncludePathCompletionContext(const DUContextPointer& context,
0252                                                            const ParseSessionData::Ptr& sessionData,
0253                                                            const QUrl& url,
0254                                                            const KTextEditor::Cursor& position,
0255                                                            const QString& text)
0256     : CodeCompletionContext(context, text, CursorInRevision::castFromSimpleCursor(position), 0)
0257 {
0258     const IncludePathProperties properties = IncludePathProperties::parseText(text);
0259 
0260     if (!properties.valid) {
0261         return;
0262     }
0263 
0264     m_includeItems = includeItemsForUrl(url, properties, sessionData->environment().includes());
0265 }
0266 
0267 QList< CompletionTreeItemPointer > IncludePathCompletionContext::completionItems(bool& abort, bool)
0268 {
0269     QList<CompletionTreeItemPointer> items;
0270 
0271     for (const auto& includeItem: qAsConst(m_includeItems)) {
0272         if (abort) {
0273             return items;
0274         }
0275 
0276         items << CompletionTreeItemPointer(new IncludeFileCompletionItem(includeItem));
0277     }
0278 
0279     return items;
0280 }