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 "autocompleter.h"
27 #include "textdocumentlayout.h"
28 #include "tabsettings.h"
29 
30 #include <QDebug>
31 #include <QTextCursor>
32 
33 using namespace TextEditor;
34 
AutoCompleter()35 AutoCompleter::AutoCompleter() :
36     m_allowSkippingOfBlockEnd(false),
37     m_autoInsertBrackets(true),
38     m_surroundWithBrackets(true),
39     m_autoInsertQuotes(true),
40     m_surroundWithQuotes(true),
41     m_overwriteClosingChars(false)
42 {}
43 
44 AutoCompleter::~AutoCompleter() = default;
45 
countBracket(QChar open,QChar close,QChar c,int * errors,int * stillopen)46 static void countBracket(QChar open, QChar close, QChar c, int *errors, int *stillopen)
47 {
48     if (c == open)
49         ++*stillopen;
50     else if (c == close)
51         --*stillopen;
52 
53     if (*stillopen < 0) {
54         *errors += -1 * (*stillopen);
55         *stillopen = 0;
56     }
57 }
58 
countBrackets(QTextCursor cursor,int from,int end,QChar open,QChar close,int * errors,int * stillopen)59 static void countBrackets(QTextCursor cursor, int from, int end, QChar open, QChar close,
60                           int *errors, int *stillopen)
61 {
62     cursor.setPosition(from);
63     QTextBlock block = cursor.block();
64     while (block.isValid() && block.position() < end) {
65         Parentheses parenList = TextDocumentLayout::parentheses(block);
66         if (!parenList.isEmpty() && !TextDocumentLayout::ifdefedOut(block)) {
67             for (int i = 0; i < parenList.count(); ++i) {
68                 Parenthesis paren = parenList.at(i);
69                 int position = block.position() + paren.pos;
70                 if (position < from || position >= end)
71                     continue;
72                 countBracket(open, close, paren.chr, errors, stillopen);
73             }
74         }
75         block = block.next();
76     }
77 }
78 
79 enum class CharType { OpenChar, CloseChar };
charType(const QChar & c,CharType type)80 static QChar charType(const QChar &c, CharType type)
81 {
82     switch (c.unicode()) {
83     case '(': case ')':
84         return type == CharType::OpenChar ? QLatin1Char('(') : QLatin1Char(')');
85     case '[': case ']':
86         return type == CharType::OpenChar ? QLatin1Char('[') : QLatin1Char(']');
87     case '{': case '}':
88         return type == CharType::OpenChar ? QLatin1Char('{') : QLatin1Char('}');
89     }
90     return QChar();
91 }
92 
fixesBracketsError(const QString & textToInsert,const QTextCursor & cursor)93 static bool fixesBracketsError(const QString &textToInsert, const QTextCursor &cursor)
94 {
95     const QChar character = textToInsert.at(0);
96     const QString allParentheses = QLatin1String("()[]{}");
97     if (!allParentheses.contains(character))
98         return false;
99 
100     QTextCursor tmp = cursor;
101     bool foundBlockStart = TextBlockUserData::findPreviousBlockOpenParenthesis(&tmp);
102     int blockStart = foundBlockStart ? tmp.position() : 0;
103     tmp = cursor;
104     bool foundBlockEnd = TextBlockUserData::findNextBlockClosingParenthesis(&tmp);
105     int blockEnd = foundBlockEnd ? tmp.position() : (cursor.document()->characterCount() - 1);
106     const QChar openChar = charType(character, CharType::OpenChar);
107     const QChar closeChar = charType(character, CharType::CloseChar);
108 
109     int errors = 0;
110     int stillopen = 0;
111     countBrackets(cursor, blockStart, blockEnd, openChar, closeChar, &errors, &stillopen);
112     int errorsBeforeInsertion = errors + stillopen;
113     errors = 0;
114     stillopen = 0;
115     countBrackets(cursor, blockStart, cursor.position(), openChar, closeChar, &errors, &stillopen);
116     countBracket(openChar, closeChar, character, &errors, &stillopen);
117     countBrackets(cursor, cursor.position(), blockEnd, openChar, closeChar, &errors, &stillopen);
118     int errorsAfterInsertion = errors + stillopen;
119     return errorsAfterInsertion < errorsBeforeInsertion;
120 }
121 
surroundSelectionWithBrackets(const QString & textToInsert,const QString & selection)122 static QString surroundSelectionWithBrackets(const QString &textToInsert, const QString &selection)
123 {
124     QString replacement;
125     if (textToInsert == QLatin1String("(")) {
126         replacement = selection + QLatin1Char(')');
127     } else if (textToInsert == QLatin1String("[")) {
128         replacement = selection + QLatin1Char(']');
129     } else if (textToInsert == QLatin1String("{")) {
130         //If the text spans multiple lines, insert on different lines
131         replacement = selection;
132         if (selection.contains(QChar::ParagraphSeparator)) {
133             //Also, try to simulate auto-indent
134             replacement = (selection.startsWith(QChar::ParagraphSeparator)
135                    ? QString()
136                    : QString(QChar::ParagraphSeparator)) + selection;
137             if (replacement.endsWith(QChar::ParagraphSeparator))
138                 replacement += QLatin1Char('}') + QString(QChar::ParagraphSeparator);
139             else
140                 replacement += QString(QChar::ParagraphSeparator) + QLatin1Char('}');
141         } else {
142             replacement += QLatin1Char('}');
143         }
144     }
145     return replacement;
146 }
147 
isQuote(const QString & text)148 bool AutoCompleter::isQuote(const QString &text)
149 {
150     return text == QLatin1String("\"") || text == QLatin1String("'");
151 }
152 
isNextBlockIndented(const QTextBlock & currentBlock) const153 bool AutoCompleter::isNextBlockIndented(const QTextBlock &currentBlock) const
154 {
155     QTextBlock block = currentBlock;
156     int indentation = m_tabSettings.indentationColumn(block.text());
157 
158     if (block.next().isValid()) { // not the last block
159         block = block.next();
160         //skip all empty blocks
161         while (block.isValid() && TabSettings::onlySpace(block.text()))
162             block = block.next();
163         if (block.isValid()
164                 && m_tabSettings.indentationColumn(block.text()) > indentation)
165             return true;
166     }
167 
168     return false;
169 }
170 
replaceSelection(QTextCursor & cursor,const QString & textToInsert) const171 QString AutoCompleter::replaceSelection(QTextCursor &cursor, const QString &textToInsert) const
172 {
173     if (!cursor.hasSelection())
174         return QString();
175     if (isQuote(textToInsert) && m_surroundWithQuotes)
176         return cursor.selectedText() + textToInsert;
177     if (m_surroundWithBrackets)
178         return surroundSelectionWithBrackets(textToInsert, cursor.selectedText());
179     return QString();
180 }
181 
autoComplete(QTextCursor & cursor,const QString & textToInsert,bool skipChars) const182 QString AutoCompleter::autoComplete(QTextCursor &cursor, const QString &textToInsert,
183                                     bool skipChars) const
184 {
185     const bool checkBlockEnd = m_allowSkippingOfBlockEnd;
186     m_allowSkippingOfBlockEnd = false; // consume blockEnd.
187 
188     QString autoText = replaceSelection(cursor, textToInsert);
189     if (!autoText.isEmpty())
190         return autoText;
191 
192     QTextDocument *doc = cursor.document();
193     const QChar lookAhead = doc->characterAt(cursor.selectionEnd());
194 
195     if (m_overwriteClosingChars && (textToInsert == lookAhead))
196         skipChars = true;
197 
198     int skippedChars = 0;
199 
200     if (isQuote(textToInsert) && m_autoInsertQuotes
201             && contextAllowsAutoQuotes(cursor, textToInsert)) {
202         autoText = insertMatchingQuote(cursor, textToInsert, lookAhead, skipChars, &skippedChars);
203     } else if (m_autoInsertBrackets && contextAllowsAutoBrackets(cursor, textToInsert)) {
204         if (fixesBracketsError(textToInsert, cursor))
205             return QString();
206 
207         autoText = insertMatchingBrace(cursor, textToInsert, lookAhead, skipChars, &skippedChars);
208 
209         if (checkBlockEnd && textToInsert.at(0) == QLatin1Char('}')) {
210             if (textToInsert.length() > 1)
211                 qWarning() << "*** handle event compression";
212 
213             int startPos = cursor.selectionEnd(), pos = startPos;
214             while (doc->characterAt(pos).isSpace())
215                 ++pos;
216 
217             if (doc->characterAt(pos) == QLatin1Char('}') && skipChars)
218                 skippedChars += (pos - startPos) + 1;
219         }
220     } else {
221         return QString();
222     }
223 
224     if (skipChars && skippedChars) {
225         const int pos = cursor.position();
226         cursor.setPosition(pos + skippedChars);
227         cursor.setPosition(pos, QTextCursor::KeepAnchor);
228     }
229 
230     return autoText;
231 }
232 
autoBackspace(QTextCursor & cursor)233 bool AutoCompleter::autoBackspace(QTextCursor &cursor)
234 {
235     m_allowSkippingOfBlockEnd = false;
236 
237     if (!m_autoInsertBrackets)
238         return false;
239 
240     int pos = cursor.position();
241     if (pos == 0)
242         return false;
243     QTextCursor c = cursor;
244     c.setPosition(pos - 1);
245 
246     QTextDocument *doc = cursor.document();
247     const QChar lookAhead = doc->characterAt(pos);
248     const QChar lookBehind = doc->characterAt(pos - 1);
249     const QChar lookFurtherBehind = doc->characterAt(pos - 2);
250 
251     const QChar character = lookBehind;
252     if (character == QLatin1Char('(')
253             || character == QLatin1Char('[')
254             || character == QLatin1Char('{')) {
255         QTextCursor tmp = cursor;
256         TextBlockUserData::findPreviousBlockOpenParenthesis(&tmp);
257         int blockStart = tmp.isNull() ? 0 : tmp.position();
258         tmp = cursor;
259         TextBlockUserData::findNextBlockClosingParenthesis(&tmp);
260         int blockEnd = tmp.isNull() ? (cursor.document()->characterCount()-1) : tmp.position();
261         QChar openChar = character;
262         QChar closeChar = charType(character, CharType::CloseChar);
263 
264         int errors = 0;
265         int stillopen = 0;
266         countBrackets(cursor, blockStart, blockEnd, openChar, closeChar, &errors, &stillopen);
267         int errorsBeforeDeletion = errors + stillopen;
268         errors = 0;
269         stillopen = 0;
270         countBrackets(cursor, blockStart, pos - 1, openChar, closeChar, &errors, &stillopen);
271         countBrackets(cursor, pos, blockEnd, openChar, closeChar, &errors, &stillopen);
272         int errorsAfterDeletion = errors + stillopen;
273 
274         if (errorsAfterDeletion < errorsBeforeDeletion)
275             return false; // insertion fixes parentheses or bracket errors, do not auto complete
276     }
277 
278     // ### this code needs to be generalized
279     if    ((lookBehind == QLatin1Char('(') && lookAhead == QLatin1Char(')'))
280         || (lookBehind == QLatin1Char('[') && lookAhead == QLatin1Char(']'))
281         || (lookBehind == QLatin1Char('{') && lookAhead == QLatin1Char('}'))
282         || (lookBehind == QLatin1Char('"') && lookAhead == QLatin1Char('"')
283             && lookFurtherBehind != QLatin1Char('\\'))
284         || (lookBehind == QLatin1Char('\'') && lookAhead == QLatin1Char('\'')
285             && lookFurtherBehind != QLatin1Char('\\'))) {
286         if (! isInComment(c)) {
287             cursor.beginEditBlock();
288             cursor.deleteChar();
289             cursor.deletePreviousChar();
290             cursor.endEditBlock();
291             return true;
292         }
293     }
294     return false;
295 }
296 
paragraphSeparatorAboutToBeInserted(QTextCursor & cursor)297 int AutoCompleter::paragraphSeparatorAboutToBeInserted(QTextCursor &cursor)
298 {
299     if (!m_autoInsertBrackets)
300         return 0;
301 
302     QTextDocument *doc = cursor.document();
303     if (doc->characterAt(cursor.position() - 1) != QLatin1Char('{'))
304         return 0;
305 
306     if (!contextAllowsAutoBrackets(cursor))
307         return 0;
308 
309     // verify that we indeed do have an extra opening brace in the document
310     QTextBlock block = cursor.block();
311     const QString textFromCusror = block.text().mid(cursor.positionInBlock()).trimmed();
312     int braceDepth = TextDocumentLayout::braceDepth(doc->lastBlock());
313 
314     if (braceDepth <= 0 && (textFromCusror.isEmpty() || textFromCusror.at(0) != QLatin1Char('}')))
315         return 0; // braces are all balanced or worse, no need to do anything and separator inserted not between '{' and '}'
316 
317     // we have an extra brace , let's see if we should close it
318 
319     /* verify that the next block is not further intended compared to the current block.
320        This covers the following case:
321 
322             if (condition) {|
323                 statement;
324     */
325     if (isNextBlockIndented(block))
326         return 0;
327 
328     const QString &textToInsert = insertParagraphSeparator(cursor);
329     int pos = cursor.position();
330     cursor.insertBlock();
331     cursor.insertText(textToInsert);
332     cursor.setPosition(pos);
333 
334     // if we actually insert a separator, allow it to be overwritten if
335     // user types it
336     if (!textToInsert.isEmpty())
337         m_allowSkippingOfBlockEnd = true;
338 
339     return 1;
340 }
341 
contextAllowsAutoBrackets(const QTextCursor & cursor,const QString & textToInsert) const342 bool AutoCompleter::contextAllowsAutoBrackets(const QTextCursor &cursor,
343                                               const QString &textToInsert) const
344 {
345     Q_UNUSED(cursor)
346     Q_UNUSED(textToInsert)
347     return false;
348 }
349 
contextAllowsAutoQuotes(const QTextCursor & cursor,const QString & textToInsert) const350 bool AutoCompleter::contextAllowsAutoQuotes(const QTextCursor &cursor, const QString &textToInsert) const
351 {
352     Q_UNUSED(cursor)
353     Q_UNUSED(textToInsert)
354     return false;
355 }
356 
contextAllowsElectricCharacters(const QTextCursor & cursor) const357 bool AutoCompleter::contextAllowsElectricCharacters(const QTextCursor &cursor) const
358 {
359     return contextAllowsAutoBrackets(cursor);
360 }
361 
isInComment(const QTextCursor & cursor) const362 bool AutoCompleter::isInComment(const QTextCursor &cursor) const
363 {
364     Q_UNUSED(cursor)
365     return false;
366 }
367 
isInString(const QTextCursor & cursor) const368 bool AutoCompleter::isInString(const QTextCursor &cursor) const
369 {
370     Q_UNUSED(cursor)
371     return false;
372 }
373 
insertMatchingBrace(const QTextCursor & cursor,const QString & text,QChar lookAhead,bool skipChars,int * skippedChars) const374 QString AutoCompleter::insertMatchingBrace(const QTextCursor &cursor,
375                                            const QString &text,
376                                            QChar lookAhead,
377                                            bool skipChars,
378                                            int *skippedChars) const
379 {
380     Q_UNUSED(cursor)
381     Q_UNUSED(text)
382     Q_UNUSED(lookAhead)
383     Q_UNUSED(skipChars)
384     Q_UNUSED(skippedChars)
385     return QString();
386 }
387 
insertMatchingQuote(const QTextCursor & cursor,const QString & text,QChar lookAhead,bool skipChars,int * skippedChars) const388 QString AutoCompleter::insertMatchingQuote(const QTextCursor &cursor,
389                                            const QString &text,
390                                            QChar lookAhead,
391                                            bool skipChars,
392                                            int *skippedChars) const
393 {
394     Q_UNUSED(cursor)
395     Q_UNUSED(text)
396     Q_UNUSED(lookAhead)
397     Q_UNUSED(skipChars)
398     Q_UNUSED(skippedChars)
399     return QString();
400 }
401 
insertParagraphSeparator(const QTextCursor & cursor) const402 QString AutoCompleter::insertParagraphSeparator(const QTextCursor &cursor) const
403 {
404     Q_UNUSED(cursor)
405     return QString();
406 }
407