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 ¤tBlock) 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