1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "clangassistproposalitem.h"
27
28 #include "clangcompletionchunkstotextconverter.h"
29 #include "clangfixitoperation.h"
30 #include "clangutils.h"
31
32 #include <cplusplus/Icons.h>
33 #include <cplusplus/MatchingText.h>
34 #include <cplusplus/SimpleLexer.h>
35 #include <cplusplus/Token.h>
36
37 #include <texteditor/completionsettings.h>
38 #include <texteditor/texteditor.h>
39 #include <texteditor/texteditorsettings.h>
40
41 #include <QCoreApplication>
42 #include <QTextBlock>
43 #include <QTextCursor>
44 #include <QTextDocument>
45
46 #include <utils/algorithm.h>
47 #include <utils/textutils.h>
48 #include <utils/qtcassert.h>
49
50 using namespace CPlusPlus;
51 using namespace ClangBackEnd;
52 using namespace TextEditor;
53 using namespace Utils;
54
55 namespace ClangCodeModel {
56 namespace Internal {
57
prematurelyApplies(const QChar & typedCharacter) const58 bool ClangAssistProposalItem::prematurelyApplies(const QChar &typedCharacter) const
59 {
60 bool applies = false;
61
62 if (m_completionOperator == T_SIGNAL || m_completionOperator == T_SLOT)
63 applies = QString::fromLatin1("(,").contains(typedCharacter);
64 else if (m_completionOperator == T_STRING_LITERAL || m_completionOperator == T_ANGLE_STRING_LITERAL)
65 applies = (typedCharacter == QLatin1Char('/')) && text().endsWith(QLatin1Char('/'));
66 else if (firstCodeCompletion().completionKind == CodeCompletion::ObjCMessageCompletionKind)
67 applies = QString::fromLatin1(";.,").contains(typedCharacter);
68 else
69 applies = QString::fromLatin1(";.,:(").contains(typedCharacter);
70
71 if (applies)
72 m_typedCharacter = typedCharacter;
73
74 return applies;
75 }
76
implicitlyApplies() const77 bool ClangAssistProposalItem::implicitlyApplies() const
78 {
79 return true;
80 }
81
textUntilPreviousStatement(TextDocumentManipulatorInterface & manipulator,int startPosition)82 static QString textUntilPreviousStatement(TextDocumentManipulatorInterface &manipulator,
83 int startPosition)
84 {
85 static const QString stopCharacters(";{}#");
86
87 int endPosition = 0;
88 for (int i = startPosition; i >= 0 ; --i) {
89 if (stopCharacters.contains(manipulator.characterAt(i))) {
90 endPosition = i + 1;
91 break;
92 }
93 }
94
95 return manipulator.textAt(endPosition, startPosition - endPosition);
96 }
97
98 // 7.3.3: using typename(opt) nested-name-specifier unqualified-id ;
isAtUsingDeclaration(TextDocumentManipulatorInterface & manipulator,int basePosition)99 static bool isAtUsingDeclaration(TextDocumentManipulatorInterface &manipulator,
100 int basePosition)
101 {
102 SimpleLexer lexer;
103 lexer.setLanguageFeatures(LanguageFeatures::defaultFeatures());
104 const QString textToLex = textUntilPreviousStatement(manipulator, basePosition);
105 const Tokens tokens = lexer(textToLex);
106 if (tokens.empty())
107 return false;
108
109 // The nested-name-specifier always ends with "::", so check for this first.
110 const Token lastToken = tokens[tokens.size() - 1];
111 if (lastToken.kind() != T_COLON_COLON)
112 return false;
113
114 return contains(tokens, [](const Token &token) { return token.kind() == T_USING; });
115 }
116
methodDefinitionParameters(const CodeCompletionChunks & chunks)117 static QString methodDefinitionParameters(const CodeCompletionChunks &chunks)
118 {
119 QString result;
120
121 auto typedTextChunkIt = std::find_if(chunks.cbegin(), chunks.cend(),
122 [](const CodeCompletionChunk &chunk) {
123 return chunk.kind == CodeCompletionChunk::TypedText;
124 });
125 if (typedTextChunkIt == chunks.cend())
126 return result;
127
128 std::for_each(++typedTextChunkIt, chunks.cend(), [&result](const CodeCompletionChunk &chunk) {
129 if (chunk.kind == CodeCompletionChunk::Placeholder && chunk.text.contains('=')) {
130 Utf8String text = chunk.text.mid(0, chunk.text.indexOf('='));
131 if (text.endsWith(' '))
132 text.chop(1);
133 result += text;
134 } else {
135 result += chunk.text;
136 }
137 });
138
139 return result;
140 }
141
skipParenForFunctionLikeSnippet(const std::vector<int> & placeholderPositions,const QString & text,int position)142 static bool skipParenForFunctionLikeSnippet(const std::vector<int> &placeholderPositions,
143 const QString &text,
144 int position)
145 {
146 return placeholderPositions.size() == 1
147 && position > 0
148 && text[position - 1] == '('
149 && text[position] == ')'
150 && position + 1 == text.size();
151 }
152
isFuncDeclAsSingleTypedText(const CodeCompletion & completion)153 static bool isFuncDeclAsSingleTypedText(const CodeCompletion &completion)
154 {
155 // There is no libclang API to tell function call items from declaration items apart.
156 // However, the chunks differ for these items (c-index-test -code-completion-at=...):
157 // An (override) declaration (available in derived class scope):
158 // CXXMethod:{TypedText void hello() override} (40)
159 // A function call:
160 // CXXMethod:{ResultType void}{TypedText hello}{LeftParen (}{RightParen )} (36)
161 return completion.completionKind == CodeCompletion::FunctionDefinitionCompletionKind
162 && completion.chunks.size() == 1
163 && completion.chunks[0].kind == CodeCompletionChunk::TypedText;
164 }
165
apply(TextDocumentManipulatorInterface & manipulator,int basePosition) const166 void ClangAssistProposalItem::apply(TextDocumentManipulatorInterface &manipulator,
167 int basePosition) const
168 {
169 const CodeCompletion ccr = firstCodeCompletion();
170
171 if (!ccr.requiredFixIts.empty()) {
172 // Important: Calculate shift before changing the document.
173 basePosition += fixItsShift(manipulator);
174
175 ClangFixItOperation fixItOperation(Utf8String(), ccr.requiredFixIts);
176 fixItOperation.perform();
177 }
178
179 QString textToBeInserted = m_text;
180 QString extraCharacters;
181 int extraLength = 0;
182 int cursorOffset = 0;
183 bool setAutoCompleteSkipPos = false;
184 int currentPosition = manipulator.currentPosition();
185
186 if (m_completionOperator == T_SIGNAL || m_completionOperator == T_SLOT) {
187 extraCharacters += QLatin1Char(')');
188 if (m_typedCharacter == QLatin1Char('(')) // Eat the opening parenthesis
189 m_typedCharacter = QChar();
190 } else if (ccr.completionKind == CodeCompletion::KeywordCompletionKind) {
191 CompletionChunksToTextConverter converter;
192 converter.setupForKeywords();
193 converter.parseChunks(ccr.chunks);
194
195 textToBeInserted = converter.text();
196
197 if (converter.hasPlaceholderPositions()) {
198 const std::vector<int> &placeholderPositions = converter.placeholderPositions();
199 const int position = placeholderPositions[0];
200 cursorOffset = position - converter.text().size();
201 // If the snippet looks like a function call, e.g. "sizeof(<PLACEHOLDER>)",
202 // ensure that we can "overtype" ')' after inserting it.
203 setAutoCompleteSkipPos = skipParenForFunctionLikeSnippet(placeholderPositions,
204 textToBeInserted,
205 position);
206 }
207 } else if (ccr.completionKind == CodeCompletion::NamespaceCompletionKind) {
208 CompletionChunksToTextConverter converter;
209 converter.parseChunks(ccr.chunks); // Appends "::" after name space name
210 textToBeInserted = converter.text();
211
212 // Clang does not provide the "::" chunk consistently for namespaces, e.g.
213 //
214 // namespace a { namespace b { namespace c {} } }
215 // <CURSOR> // complete "a" ==> "a::"
216 // a::<CURSOR> // complete "b" ==> "b", not "b::"
217 //
218 // Remove "::" to avoid any confusion for now.
219 if (textToBeInserted.endsWith("::"))
220 textToBeInserted.chop(2);
221 } else if (!ccr.text.isEmpty()) {
222 const CompletionSettings &completionSettings =
223 TextEditorSettings::completionSettings();
224 const bool autoInsertBrackets = completionSettings.m_autoInsertBrackets;
225
226 if (autoInsertBrackets &&
227 (ccr.completionKind == CodeCompletion::FunctionCompletionKind
228 || ccr.completionKind == CodeCompletion::FunctionDefinitionCompletionKind
229 || ccr.completionKind == CodeCompletion::DestructorCompletionKind
230 || ccr.completionKind == CodeCompletion::ConstructorCompletionKind
231 || ccr.completionKind == CodeCompletion::SignalCompletionKind
232 || ccr.completionKind == CodeCompletion::SlotCompletionKind)) {
233 // When the user typed the opening parenthesis, he'll likely also type the closing one,
234 // in which case it would be annoying if we put the cursor after the already automatically
235 // inserted closing parenthesis.
236 const bool skipClosingParenthesis = m_typedCharacter != QLatin1Char('(');
237 QTextCursor cursor = manipulator.textCursorAt(basePosition);
238
239 bool abandonParen = false;
240 if (matchPreviousWord(manipulator, cursor, "&")) {
241 moveToPreviousWord(manipulator, cursor);
242 moveToPreviousChar(manipulator, cursor);
243 const QChar prevChar = manipulator.characterAt(cursor.position());
244 cursor.setPosition(basePosition);
245 abandonParen = QString("(;,{}").contains(prevChar);
246 }
247 if (!abandonParen) {
248 const bool isFullDecl = isFuncDeclAsSingleTypedText(ccr);
249 if (isFullDecl)
250 extraCharacters += QLatin1Char(';');
251 abandonParen = isAtUsingDeclaration(manipulator, basePosition) || isFullDecl;
252 }
253
254 if (!abandonParen && ccr.completionKind == CodeCompletion::FunctionDefinitionCompletionKind) {
255 const CodeCompletionChunk resultType = ccr.chunks.first();
256 if (resultType.kind == CodeCompletionChunk::ResultType) {
257 if (matchPreviousWord(manipulator, cursor, resultType.text.toString())) {
258 extraCharacters += methodDefinitionParameters(ccr.chunks);
259 // To skip the next block.
260 abandonParen = true;
261 }
262 } else {
263 // Do nothing becasue it's not a function definition.
264
265 // It's a clang bug that the function might miss a ResultType chunk
266 // when the base class method is called from the overriding method
267 // of the derived class. For example:
268 // void Derived::foo() override { Base::<complete here> }
269 }
270 }
271 if (!abandonParen) {
272 if (completionSettings.m_spaceAfterFunctionName)
273 extraCharacters += QLatin1Char(' ');
274 extraCharacters += QLatin1Char('(');
275 if (m_typedCharacter == QLatin1Char('('))
276 m_typedCharacter = QChar();
277
278 // If the function doesn't return anything, automatically place the semicolon,
279 // unless we're doing a scope completion (then it might be function definition).
280 const QChar characterAtCursor = manipulator.characterAt(currentPosition);
281 bool endWithSemicolon = m_typedCharacter == QLatin1Char(';')/*
282 || (function->returnType()->isVoidType() && m_completionOperator != T_COLON_COLON)*/; //###
283 const QChar semicolon = m_typedCharacter.isNull() ? QLatin1Char(';') : m_typedCharacter;
284
285 if (endWithSemicolon && characterAtCursor == semicolon) {
286 endWithSemicolon = false;
287 m_typedCharacter = QChar();
288 }
289
290 // If the function takes no arguments, automatically place the closing parenthesis
291 if (!hasOverloadsWithParameters() && !ccr.hasParameters && skipClosingParenthesis) {
292 extraCharacters += QLatin1Char(')');
293 if (endWithSemicolon) {
294 extraCharacters += semicolon;
295 m_typedCharacter = QChar();
296 }
297 } else {
298 const QChar lookAhead = manipulator.characterAt(manipulator.currentPosition() + 1);
299 if (MatchingText::shouldInsertMatchingText(lookAhead)) {
300 extraCharacters += QLatin1Char(')');
301 --cursorOffset;
302 setAutoCompleteSkipPos = true;
303 if (endWithSemicolon) {
304 extraCharacters += semicolon;
305 --cursorOffset;
306 m_typedCharacter = QChar();
307 }
308 }
309 }
310 }
311 }
312 }
313
314 // Append an unhandled typed character, adjusting cursor offset when it had been adjusted before
315 if (!m_typedCharacter.isNull()) {
316 extraCharacters += m_typedCharacter;
317 if (cursorOffset != 0)
318 --cursorOffset;
319 }
320
321 // Avoid inserting characters that are already there
322 QTextCursor cursor = manipulator.textCursorAt(basePosition);
323 cursor.movePosition(QTextCursor::EndOfWord);
324 const QString textAfterCursor = manipulator.textAt(currentPosition,
325 cursor.position() - currentPosition);
326
327 if (textToBeInserted != textAfterCursor
328 && textToBeInserted.indexOf(textAfterCursor, currentPosition - basePosition) >= 0) {
329 currentPosition = cursor.position();
330 }
331
332 for (int i = 0; i < extraCharacters.length(); ++i) {
333 const QChar a = extraCharacters.at(i);
334 const QChar b = manipulator.characterAt(currentPosition + i);
335 if (a == b)
336 ++extraLength;
337 else
338 break;
339 }
340
341 textToBeInserted += extraCharacters;
342
343 const int length = currentPosition - basePosition + extraLength;
344
345 const bool isReplaced = manipulator.replace(basePosition, length, textToBeInserted);
346 manipulator.setCursorPosition(basePosition + textToBeInserted.length());
347 if (isReplaced) {
348 if (cursorOffset)
349 manipulator.setCursorPosition(manipulator.currentPosition() + cursorOffset);
350 if (setAutoCompleteSkipPos)
351 manipulator.setAutoCompleteSkipPosition(manipulator.currentPosition());
352
353 if (ccr.completionKind == CodeCompletion::KeywordCompletionKind)
354 manipulator.autoIndent(basePosition, textToBeInserted.size());
355 }
356 }
357
setText(const QString & text)358 void ClangAssistProposalItem::setText(const QString &text)
359 {
360 m_text = text;
361 }
362
text() const363 QString ClangAssistProposalItem::text() const
364 {
365 return m_text;
366 }
367
firstCompletionFixIts() const368 const QVector<ClangBackEnd::FixItContainer> &ClangAssistProposalItem::firstCompletionFixIts() const
369 {
370 return firstCodeCompletion().requiredFixIts;
371 }
372
fixItPositionsRange(const FixItContainer & fixIt,const QTextCursor & cursor)373 std::pair<int, int> fixItPositionsRange(const FixItContainer &fixIt, const QTextCursor &cursor)
374 {
375 const QTextBlock startLine = cursor.document()->findBlockByNumber(fixIt.range.start.line - 1);
376 const QTextBlock endLine = cursor.document()->findBlockByNumber(fixIt.range.end.line - 1);
377
378 const int fixItStartPos = Text::positionInText(
379 cursor.document(),
380 fixIt.range.start.line,
381 cppEditorColumn(startLine, fixIt.range.start.column));
382 const int fixItEndPos = Text::positionInText(
383 cursor.document(),
384 fixIt.range.end.line,
385 cppEditorColumn(endLine, fixIt.range.end.column));
386 return std::make_pair(fixItStartPos, fixItEndPos);
387 }
388
textReplacedByFixit(const FixItContainer & fixIt)389 static QString textReplacedByFixit(const FixItContainer &fixIt)
390 {
391 TextEditorWidget *textEditorWidget = TextEditorWidget::currentTextEditorWidget();
392 if (!textEditorWidget)
393 return QString();
394 const std::pair<int, int> fixItPosRange = fixItPositionsRange(fixIt,
395 textEditorWidget->textCursor());
396 return textEditorWidget->textAt(fixItPosRange.first,
397 fixItPosRange.second - fixItPosRange.first);
398 }
399
fixItText() const400 QString ClangAssistProposalItem::fixItText() const
401 {
402 const FixItContainer &fixIt = firstCompletionFixIts().first();
403 return QCoreApplication::translate("ClangCodeModel::ClangAssistProposalItem",
404 "Requires changing \"%1\" to \"%2\"")
405 .arg(textReplacedByFixit(fixIt), fixIt.text.toString());
406 }
407
fixItsShift(const TextDocumentManipulatorInterface & manipulator) const408 int ClangAssistProposalItem::fixItsShift(const TextDocumentManipulatorInterface &manipulator) const
409 {
410 const QVector<ClangBackEnd::FixItContainer> &requiredFixIts = firstCompletionFixIts();
411 if (requiredFixIts.empty())
412 return 0;
413
414 int shift = 0;
415 const QTextCursor cursor = manipulator.textCursorAt(0);
416 for (const FixItContainer &fixIt : requiredFixIts) {
417 const std::pair<int, int> fixItPosRange = fixItPositionsRange(fixIt, cursor);
418 shift += fixIt.text.toString().length() - (fixItPosRange.second - fixItPosRange.first);
419 }
420 return shift;
421 }
422
icon() const423 QIcon ClangAssistProposalItem::icon() const
424 {
425 using namespace CPlusPlus::Icons;
426 static const char SNIPPET_ICON_PATH[] = ":/texteditor/images/snippet.png";
427 static const QIcon snippetIcon = QIcon(QLatin1String(SNIPPET_ICON_PATH));
428
429 const ClangBackEnd::CodeCompletion &completion = firstCodeCompletion();
430 switch (completion.completionKind) {
431 case CodeCompletion::ClassCompletionKind:
432 case CodeCompletion::TemplateClassCompletionKind:
433 case CodeCompletion::TypeAliasCompletionKind:
434 return CodeModelIcon::iconForType(CodeModelIcon::Class);
435 case CodeCompletion::EnumerationCompletionKind:
436 return CodeModelIcon::iconForType(CodeModelIcon::Enum);
437 case CodeCompletion::EnumeratorCompletionKind:
438 return CodeModelIcon::iconForType(CodeModelIcon::Enumerator);
439 case CodeCompletion::ConstructorCompletionKind:
440 case CodeCompletion::DestructorCompletionKind:
441 case CodeCompletion::FunctionCompletionKind:
442 case CodeCompletion::FunctionDefinitionCompletionKind:
443 case CodeCompletion::TemplateFunctionCompletionKind:
444 case CodeCompletion::ObjCMessageCompletionKind:
445 switch (completion.availability) {
446 case CodeCompletion::Available:
447 case CodeCompletion::Deprecated:
448 return CodeModelIcon::iconForType(CodeModelIcon::FuncPublic);
449 default:
450 return CodeModelIcon::iconForType(CodeModelIcon::FuncPrivate);
451 }
452 case CodeCompletion::SignalCompletionKind:
453 return CodeModelIcon::iconForType(CodeModelIcon::Signal);
454 case CodeCompletion::SlotCompletionKind:
455 switch (completion.availability) {
456 case CodeCompletion::Available:
457 case CodeCompletion::Deprecated:
458 return CodeModelIcon::iconForType(CodeModelIcon::SlotPublic);
459 case CodeCompletion::NotAccessible:
460 case CodeCompletion::NotAvailable:
461 return CodeModelIcon::iconForType(CodeModelIcon::SlotPrivate);
462 }
463 break;
464 case CodeCompletion::NamespaceCompletionKind:
465 return CodeModelIcon::iconForType(CodeModelIcon::Namespace);
466 case CodeCompletion::PreProcessorCompletionKind:
467 return CodeModelIcon::iconForType(CodeModelIcon::Macro);
468 case CodeCompletion::VariableCompletionKind:
469 switch (completion.availability) {
470 case CodeCompletion::Available:
471 case CodeCompletion::Deprecated:
472 return CodeModelIcon::iconForType(CodeModelIcon::VarPublic);
473 default:
474 return CodeModelIcon::iconForType(CodeModelIcon::VarPrivate);
475 }
476 case CodeCompletion::KeywordCompletionKind:
477 return CodeModelIcon::iconForType(CodeModelIcon::Keyword);
478 case CodeCompletion::ClangSnippetKind:
479 return snippetIcon;
480 case CodeCompletion::Other:
481 return CodeModelIcon::iconForType(CodeModelIcon::Unknown);
482 default:
483 break;
484 }
485
486 return QIcon();
487 }
488
detail() const489 QString ClangAssistProposalItem::detail() const
490 {
491 QString detail;
492 for (const ClangBackEnd::CodeCompletion &codeCompletion : m_codeCompletions) {
493 if (!detail.isEmpty())
494 detail += "<br>";
495 detail += CompletionChunksToTextConverter::convertToToolTipWithHtml(
496 codeCompletion.chunks, codeCompletion.completionKind);
497
498 if (!codeCompletion.briefComment.isEmpty())
499 detail += "<br>" + codeCompletion.briefComment.toString();
500 }
501
502 if (requiresFixIts())
503 detail += "<br><br><b>" + fixItText() + "</b>";
504
505 return detail;
506 }
507
isKeyword() const508 bool ClangAssistProposalItem::isKeyword() const
509 {
510 // KeywordCompletionKind includes real keywords but also "code patterns"/snippets.
511 return m_codeCompletions[0].completionKind == CodeCompletion::KeywordCompletionKind;
512 }
513
detailFormat() const514 Qt::TextFormat ClangAssistProposalItem::detailFormat() const
515 {
516 return Qt::RichText;
517 }
518
isSnippet() const519 bool ClangAssistProposalItem::isSnippet() const
520 {
521 return false;
522 }
523
isValid() const524 bool ClangAssistProposalItem::isValid() const
525 {
526 return true;
527 }
528
hash() const529 quint64 ClangAssistProposalItem::hash() const
530 {
531 return 0;
532 }
533
requiresFixIts() const534 bool ClangAssistProposalItem::requiresFixIts() const
535 {
536 return !firstCompletionFixIts().empty();
537 }
538
hasOverloadsWithParameters() const539 bool ClangAssistProposalItem::hasOverloadsWithParameters() const
540 {
541 return m_hasOverloadsWithParameters;
542 }
543
setHasOverloadsWithParameters(bool hasOverloadsWithParameters)544 void ClangAssistProposalItem::setHasOverloadsWithParameters(bool hasOverloadsWithParameters)
545 {
546 m_hasOverloadsWithParameters = hasOverloadsWithParameters;
547 }
548
keepCompletionOperator(unsigned compOp)549 void ClangAssistProposalItem::keepCompletionOperator(unsigned compOp)
550 {
551 m_completionOperator = compOp;
552 }
553
appendCodeCompletion(const CodeCompletion & codeCompletion)554 void ClangAssistProposalItem::appendCodeCompletion(const CodeCompletion &codeCompletion)
555 {
556 m_codeCompletions.push_back(codeCompletion);
557 }
558
firstCodeCompletion() const559 const ClangBackEnd::CodeCompletion &ClangAssistProposalItem::firstCodeCompletion() const
560 {
561 return m_codeCompletions.at(0);
562 }
563
removeFirstCodeCompletion()564 void ClangAssistProposalItem::removeFirstCodeCompletion()
565 {
566 QTC_ASSERT(!m_codeCompletions.empty(), return;);
567 m_codeCompletions.erase(m_codeCompletions.begin());
568 }
569
570 } // namespace Internal
571 } // namespace ClangCodeModel
572