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 }