1/**
2 * This file is part of the KDE libraries
3 * Copyright (C) 2008 Jakob Petsovits <jpetso@gmx.at>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License version 2 as published by the Free Software Foundation.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB. If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "autobrace.h"
21#include "autobrace_config.h"
22
23#include <kpluginfactory.h>
24#include <kpluginloader.h>
25
26#include <ktexteditor/configinterface.h>
27#include <kmessagebox.h>
28#include <klocalizedstring.h>
29#include <iostream>
30#include <kconfiggroup.h>
31
32AutoBracePlugin *AutoBracePlugin::plugin = 0;
33
34K_PLUGIN_FACTORY_DEFINITION(AutoBracePluginFactory,
35 registerPlugin<AutoBracePlugin>("ktexteditor_autobrace");
36 registerPlugin<AutoBraceConfig>("ktexteditor_autobrace_config");
37 )
38K_EXPORT_PLUGIN(AutoBracePluginFactory("ktexteditor_autobrace", "ktexteditor_plugins"))
39
40AutoBracePlugin::AutoBracePlugin(QObject *parent, const QVariantList &args)
41 : KTextEditor::Plugin(parent), m_autoBrackets(true), m_autoQuotations(true)
42{
43 Q_UNUSED(args);
44 plugin = this;
45
46 readConfig();
47}
48
49AutoBracePlugin::~AutoBracePlugin()
50{
51 plugin = 0;
52}
53
54void AutoBracePlugin::addView(KTextEditor::View *view)
55{
56 AutoBracePluginDocument *docplugin;
57
58 // We're not storing the brace inserter by view but by document,
59 // which makes signal connection and destruction a bit easier.
60 if (m_docplugins.contains(view->document())) {
61 docplugin = m_docplugins.value(view->document());
62 }
63 else {
64 // Create Editor plugin and assign options through reference
65 docplugin = new AutoBracePluginDocument(view->document(),
66 m_autoBrackets,
67 m_autoQuotations);
68 m_docplugins.insert(view->document(), docplugin);
69 }
70 // Shouldn't be necessary in theory, but for removeView() the document
71 // might already be destroyed and removed. Also used as refcounter.
72 m_documents.insert(view, view->document());
73}
74
75void AutoBracePlugin::removeView(KTextEditor::View *view)
76{
77 if (m_documents.contains(view))
78 {
79 KTextEditor::Document *document = m_documents.value(view);
80 m_documents.remove(view);
81
82 // Only detach from the document if it was the last view pointing to that.
83 if (m_documents.keys(document).empty()) {
84 AutoBracePluginDocument *docplugin = m_docplugins.value(document);
85 m_docplugins.remove(document);
86 delete docplugin;
87 }
88 }
89}
90
91void AutoBracePlugin::readConfig()
92{
93 KConfigGroup cg(KGlobal::config(), "AutoBrace Plugin");
94 m_autoBrackets = cg.readEntry("autobrackets", true);
95 m_autoQuotations = cg.readEntry("autoquotations", false);
96}
97
98void AutoBracePlugin::writeConfig()
99{
100 KConfigGroup cg(KGlobal::config(), "AutoBrace Plugin");
101 cg.writeEntry("autobrackets", m_autoBrackets);
102 cg.writeEntry("autoquotations", m_autoQuotations);
103}
104
105/// AutoBracePluginDocument
106
107AutoBracePluginDocument::AutoBracePluginDocument(KTextEditor::Document* document, const bool& autoBrackets, const bool& autoQuotations)
108 : QObject(document), m_insertionLine(0), m_withSemicolon(false),
109 m_lastRange(KTextEditor::Range::invalid()), m_autoBrackets(autoBrackets), m_autoQuotations(autoQuotations)
110{
111 connect(document, SIGNAL(exclusiveEditStart(KTextEditor::Document*)),
112 this, SLOT(disconnectSlots(KTextEditor::Document*)));
113 connect(document, SIGNAL(exclusiveEditEnd(KTextEditor::Document*)),
114 this, SLOT(connectSlots(KTextEditor::Document*)));
115
116 connectSlots(document);
117}
118
119AutoBracePluginDocument::~AutoBracePluginDocument()
120{
121 disconnect(parent() /* == document */, 0, this, 0);
122}
123
124/**
125 * (Re-)setups slots for AutoBracePluginDocument.
126 * @param document Current document.
127 */
128void AutoBracePluginDocument::connectSlots(KTextEditor::Document *document)
129{
130 connect(document, SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)),
131 this, SLOT(slotTextInserted(KTextEditor::Document*,KTextEditor::Range)));
132 connect(document, SIGNAL(textRemoved(KTextEditor::Document*,KTextEditor::Range)),
133 this, SLOT(slotTextRemoved(KTextEditor::Document*,KTextEditor::Range)));
134}
135
136void AutoBracePluginDocument::disconnectSlots(KTextEditor::Document* document)
137{
138 disconnect(document, SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)),
139 this, SLOT(slotTextInserted(KTextEditor::Document*,KTextEditor::Range)));
140 disconnect(document, SIGNAL(textRemoved(KTextEditor::Document*,KTextEditor::Range)),
141 this, SLOT(slotTextRemoved(KTextEditor::Document*,KTextEditor::Range)));
142 disconnect(document, SIGNAL(textChanged(KTextEditor::Document*)),
143 this, SLOT(slotTextChanged(KTextEditor::Document*)));
144}
145
146/**
147 * Connected to KTextEditor::Document::textChanged() once slotTextInserted()
148 * found a line with an opening brace. This takes care of inserting the new
149 * line with its closing counterpart.
150 */
151void AutoBracePluginDocument::slotTextChanged(KTextEditor::Document *document) {
152 // Disconnect from all signals as we insert stuff by ourselves.
153 // Prevent infinite recursion.
154 disconnectSlots(document);
155
156 // Make really sure that we want to insert the brace, paste guard and all.
157 if (m_insertionLine != 0
158 && m_insertionLine == document->activeView()->cursorPosition().line()
159 && document->line(m_insertionLine).trimmed().isEmpty())
160 {
161 KTextEditor::View *view = document->activeView();
162 document->startEditing();
163
164 // If the document's View is a KateView then it's able to indent.
165 // We hereby ignore the indenter and always indent correctly. (Sorry!)
166 if (view->inherits("KateView")) {
167 // Correctly indent the empty line. Magic!
168 KTextEditor::Range lineRange(
169 m_insertionLine, 0,
170 m_insertionLine, document->lineLength(m_insertionLine)
171 );
172 document->replaceText(lineRange, m_indentation);
173
174 connect(this, SIGNAL(indent()), view, SLOT(indent()));
175 emit indent();
176 disconnect(this, SIGNAL(indent()), view, SLOT(indent()));
177 }
178 // The line with the closing brace. (Inserted via insertLine() in order
179 // to avoid space removal by potential indenters.)
180 document->insertLine(m_insertionLine + 1, m_indentation + '}' + (m_withSemicolon ? ";" : ""));
181
182 document->endEditing();
183 view->setCursorPosition(document->endOfLine(m_insertionLine));
184 }
185 m_insertionLine = 0;
186
187 // Re-enable the textInserted() slot again.
188 connectSlots(document);
189}
190
191/**
192 * Connected to KTextEditor::Documet::textRemoved. Allows to delete
193 * an automatically inserted closing bracket if the opening counterpart
194 * has been removed.
195 */
196void AutoBracePluginDocument::slotTextRemoved(KTextEditor::Document* document, const KTextEditor::Range& range)
197{
198 // If last range equals the deleted text range (last range
199 // is last inserted bracket), we also delete the associated closing bracket.
200 if (m_lastRange == range) {
201 // avoid endless recursion
202 disconnectSlots(document);
203
204 // Delete the character at the same range because the opening
205 // bracket has already been removed so the closing bracket
206 // should now have been shifted to that same position
207 if (range.isValid()) {
208 document->removeText(range);
209 }
210
211 connectSlots(document);
212 }
213}
214
215/**
216 * Connected to KTextEditor::Document::textInserted(), which is emitted on all
217 * insertion changes. Line text and line breaks are emitted separately by
218 * KatePart, and pasted text gets the same treatment as manually entered text.
219 * Because of that, we discard paste operations by only remembering the
220 * insertion status for the last line that was entered.
221 */
222void AutoBracePluginDocument::slotTextInserted(KTextEditor::Document *document,
223 const KTextEditor::Range& range)
224{
225 // Fill brackets map matching opening and closing brackets.
226 QMap<QString,QString> brackets;
227 brackets["("] = ")";
228 brackets["["] = "]";
229
230 // latex wants {, too
231 if (document->mode() == "LaTeX")
232 brackets["{"] = "}";
233
234 // List of Tokens after which an automatic bracket expanion
235 // is allowed.
236 const static QStringList allowedNextToken = QStringList() << "]" << ")" << ","
237 << "." << ";" << "\n" << "\t" << " " << "";
238 const QString text = document->text(range);
239
240 // An insertion operation cancels any last range removal
241 // operation
242 m_lastRange = KTextEditor::Range::invalid();
243
244 // Make sure to handle only:
245 // 1.) New lines after { (brace openers)
246 // 2.) Opening braces like '(' and '['
247 // 3.) Quotation marks like " and '
248
249 // Handle brace openers
250 if (text == "\n") {
251 // Remember this position as insertion candidate.
252 // We don't directly insert this here because of KatePart specifics:
253 // a) Setting the cursor position crashes at this point, and
254 // b) textChanged() only gets called once per edit operation, so we can
255 // ignore the same braces when they're being inserted via paste.
256 if (isInsertionCandidate(document, range.start().line())) {
257 m_insertionLine = range.end().line();
258 connect(document, SIGNAL(textChanged(KTextEditor::Document*)),
259 this, SLOT(slotTextChanged(KTextEditor::Document*)));
260 }
261 else {
262 m_insertionLine = 0;
263 }
264 }
265 // Opening brackets (defined in ctor)
266 else if (m_autoBrackets && brackets.contains(text)) {
267 // Only insert auto closing brackets if current text range
268 // is followed by one of the allowed next tokens.
269 if (allowedNextToken.contains(nextToken(document,range))) {
270 insertAutoBracket(document, range, brackets[text]);
271 }
272
273 }
274 // Check whether closing brackets are allowed.
275 // If a brace is not allowed remove it
276 // and set the cursor to the position after that text range.
277 // Bracket tests bases on this simple idea: A bracket can only be inserted
278 // if it is NOT followed by the same bracket. This results in overwriting closing brackets.
279 else if (m_autoBrackets && brackets.values().contains(text)) {
280 if (nextToken(document,range) == text) {
281 KTextEditor::Cursor saved = range.end();
282 document->removeText(range);
283 document->activeView()->setCursorPosition(saved);
284 }
285 }
286 // Insert auto-quotation marks (if enabled). Same idea as with brackets
287 // applies here: double quotation marks are eaten up and only inserted if not
288 // followed by the same quoation mark. Additionally automatic quotation marks
289 // are inserted only if NOT followed by a back slash (escaping character).
290 else if (m_autoQuotations && (text == "\"" || text == "\'") && previousToken(document, range) != "\\") {
291 const QString next = nextToken(document, range);
292 // Eat it if already there
293 if (next == text) {
294 KTextEditor::Cursor saved = range.end();
295 document->removeText(range);
296 document->activeView()->setCursorPosition(saved);
297 }
298 // Quotation marks only inserted if followed by one of the allowed
299 // next tokens and the number of marks in the insertion line is even
300 // (excluding the already inserted mark)
301 else if (allowedNextToken.contains(next)
302 && (document->line(range.start().line()).count(text) % 2) ) {
303 insertAutoBracket(document, range, text);
304 }
305 }
306}
307
308/**
309 * Automatically inserts closing bracket. Cursor
310 * is placed in between the brackets.
311 * @param document Current document.
312 * @param range Inserted text range (by text-inserted slot)
313 * @param brace Brace to insert
314 */
315void AutoBracePluginDocument::insertAutoBracket(KTextEditor::Document *document,
316 const KTextEditor::Range& range,
317 const QString& brace) {
318 // Disconnect Slots to avoid check for redundant closing brackets
319 disconnectSlots(document);
320
321 // Save range to allow following remove operation to
322 // detect the corresponding closing bracket
323 m_lastRange = range;
324
325 KTextEditor::Cursor saved = range.end();
326 // Depending on brace, insert corresponding closing brace.
327 document->insertText(range.end(), brace);
328 document->activeView()->setCursorPosition(saved);
329
330 // Re-Enable insertion slot.
331 connectSlots(document);
332}
333
334/**
335 * Returns next character after specified text range in document.
336 * @param document Current document.
337 * @param range Inserted text range (by text-inserted slot)
338 * @return Next character after text range
339 */
340const QString AutoBracePluginDocument::nextToken(KTextEditor::Document* document, const KTextEditor::Range& range)
341{
342 // Calculate range after insertion (exactly one character)
343 KTextEditor::Range afterRange(range.end(), range.end().line(), range.end().column()+1);
344
345 return (afterRange.isValid() ? document->text(afterRange) : "");
346}
347
348/**
349 * Returns previous character before specified text range in document.
350 * @param document Current document.
351 * @param range Inserted text range (by text-inserted slot)
352 * @return Next character after text range
353 */
354const QString AutoBracePluginDocument::previousToken(KTextEditor::Document* document, const KTextEditor::Range& range)
355{
356 // Calculate range before insertion (exactly one character)
357 KTextEditor::Range beforeRange(range.start().line(), range.start().column()-1, range.start().line(),
358 range.start().column());
359
360 return (beforeRange.isValid() ? document->text(beforeRange) : "");
361}
362
363bool AutoBracePluginDocument::isInsertionCandidate(KTextEditor::Document *document, int openingBraceLine) {
364 QString line = document->line(openingBraceLine);
365 if (line.isEmpty() || !line.endsWith('{')) {
366 return false;
367 }
368
369 // Get the indentation prefix.
370 QRegExp rx("^(\\s+)");
371 QString indentation = (rx.indexIn(line) == -1) ? "" : rx.cap(1);
372
373 // Determine whether to insert a brace or not, depending on the indentation
374 // of the upcoming (non-empty) line.
375 bool isCandidate = true;
376 QString indentationLength = QString::number(indentation.length());
377 QString indentationLengthMinusOne = QString::number(indentation.length() - 1);
378
379 ///TODO: make configurable
380 // these tokens must not start a line that is used to get the correct indendation width
381 QStringList forbiddenTokenList;
382 if ( line.contains("class") || line.contains("interface") || line.contains("struct") ) {
383 forbiddenTokenList << "private" << "public" << "protected";
384 if ( document->mode() == "C++" ) {
385 forbiddenTokenList << "signals" << "Q_SIGNALS";
386 } else {
387 // PHP and potentially others
388 forbiddenTokenList << "function";
389 }
390 }
391 if ( (document->mode() == "C++" || document->mode() == "C") && line.contains("namespace", Qt::CaseInsensitive) ) {
392 // C++ specific
393 forbiddenTokenList << "class" << "struct";
394 }
395 const QString forbiddenTokens = forbiddenTokenList.isEmpty() ? QLatin1String("") : QString(QLatin1String("(?!") + forbiddenTokenList.join(QLatin1String("|")) + QLatin1Char(')'));
396
397 for (int i = openingBraceLine + 1; i < document->lines(); ++i)
398 {
399 line = document->line(i);
400 if (line.trimmed().isEmpty()) {
401 continue; // Empty lines are not a reliable source of information.
402 }
403
404 if (indentation.length() == 0) {
405 // Inserting a brace is ok if there is a line (not starting with a
406 // brace) without indentation.
407 rx.setPattern("^(?=[^\\}\\s])"
408 // But it's not OK if the line starts with one of our forbidden tokens.
409 + forbiddenTokens
410 );
411 }
412 else {
413 rx.setPattern("^(?:"
414 // Inserting a brace is ok if there is a closing brace with
415 // less indentation than the opener line.
416 "[\\s]{0," + indentationLengthMinusOne + "}\\}"
417 "|"
418 // Inserting a brace is ok if there is a line (not starting with a
419 // brace) with less or similar indentation as the original line.
420 "[\\s]{0," + indentationLength + "}(?=[^\\}\\s])"
421 // But it's not OK if the line starts with one of our forbidden tokens.
422 + forbiddenTokens +
423 ")"
424 );
425 }
426
427 if (rx.indexIn(line) == -1) {
428 // There is already a brace, or the line is indented more than the
429 // opener line (which means we expect a brace somewhere further down),
430 // or we found a forbidden token.
431 // So don't insert the brace, and just indent the line.
432 isCandidate = false;
433 }
434 // Quit the loop - a non-empty line always leads to a definitive decision.
435 break;
436 }
437
438 if (isCandidate) {
439 m_indentation = indentation;
440 // in C++ automatically add a semicolon after the closing brace when we create a new class/struct
441 if ( (document->mode() == "C++" || document->mode() == "C")
442 && document->line(openingBraceLine).indexOf(QRegExp("(?:class|struct|enum)\\s+[^\\s]+(\\s*[:,](\\s*((public|protected|private)\\s+)?[^\\s]+))*\\s*\\{\\s*$")) != -1 )
443 {
444 m_withSemicolon = true;
445 } else {
446 m_withSemicolon = false;
447 }
448 }
449 return isCandidate;
450}
451
452#include "autobrace.moc"
453