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 | |
32 | AutoBracePlugin *AutoBracePlugin::plugin = 0; |
33 | |
34 | K_PLUGIN_FACTORY_DEFINITION(AutoBracePluginFactory, |
35 | registerPlugin<AutoBracePlugin>("ktexteditor_autobrace" ); |
36 | registerPlugin<AutoBraceConfig>("ktexteditor_autobrace_config" ); |
37 | ) |
38 | K_EXPORT_PLUGIN(AutoBracePluginFactory("ktexteditor_autobrace" , "ktexteditor_plugins" )) |
39 | |
40 | AutoBracePlugin::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 | |
49 | AutoBracePlugin::~AutoBracePlugin() |
50 | { |
51 | plugin = 0; |
52 | } |
53 | |
54 | void 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 | |
75 | void 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 | |
91 | void 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 | |
98 | void 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 | |
107 | AutoBracePluginDocument::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 | |
119 | AutoBracePluginDocument::~AutoBracePluginDocument() |
120 | { |
121 | disconnect(parent() /* == document */, 0, this, 0); |
122 | } |
123 | |
124 | /** |
125 | * (Re-)setups slots for AutoBracePluginDocument. |
126 | * @param document Current document. |
127 | */ |
128 | void 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 | |
136 | void 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 | */ |
151 | void 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 | */ |
196 | void 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 | */ |
222 | void 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 | */ |
315 | void 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 | */ |
340 | const 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 | */ |
354 | const 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 | |
363 | bool 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 | |