mirror of
https://bitbucket.org/smil3y/kde-workspace.git
synced 2025-02-24 19:02:51 +00:00
452 lines
18 KiB
C++
452 lines
18 KiB
C++
/**
|
|
* This file is part of the KDE libraries
|
|
* Copyright (C) 2008 Jakob Petsovits <jpetso@gmx.at>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public License
|
|
* along with this library; see the file COPYING.LIB. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "autobrace.h"
|
|
#include "autobrace_config.h"
|
|
|
|
#include <kpluginfactory.h>
|
|
#include <kpluginloader.h>
|
|
|
|
#include <ktexteditor/configinterface.h>
|
|
#include <kmessagebox.h>
|
|
#include <klocalizedstring.h>
|
|
#include <iostream>
|
|
#include <kconfiggroup.h>
|
|
|
|
AutoBracePlugin *AutoBracePlugin::plugin = 0;
|
|
|
|
K_PLUGIN_FACTORY_DEFINITION(AutoBracePluginFactory,
|
|
registerPlugin<AutoBracePlugin>("ktexteditor_autobrace");
|
|
registerPlugin<AutoBraceConfig>("ktexteditor_autobrace_config");
|
|
)
|
|
K_EXPORT_PLUGIN(AutoBracePluginFactory("ktexteditor_autobrace", "ktexteditor_plugins"))
|
|
|
|
AutoBracePlugin::AutoBracePlugin(QObject *parent, const QVariantList &args)
|
|
: KTextEditor::Plugin(parent), m_autoBrackets(true), m_autoQuotations(true)
|
|
{
|
|
Q_UNUSED(args);
|
|
plugin = this;
|
|
|
|
readConfig();
|
|
}
|
|
|
|
AutoBracePlugin::~AutoBracePlugin()
|
|
{
|
|
plugin = 0;
|
|
}
|
|
|
|
void AutoBracePlugin::addView(KTextEditor::View *view)
|
|
{
|
|
AutoBracePluginDocument *docplugin;
|
|
|
|
// We're not storing the brace inserter by view but by document,
|
|
// which makes signal connection and destruction a bit easier.
|
|
if (m_docplugins.contains(view->document())) {
|
|
docplugin = m_docplugins.value(view->document());
|
|
}
|
|
else {
|
|
// Create Editor plugin and assign options through reference
|
|
docplugin = new AutoBracePluginDocument(view->document(),
|
|
m_autoBrackets,
|
|
m_autoQuotations);
|
|
m_docplugins.insert(view->document(), docplugin);
|
|
}
|
|
// Shouldn't be necessary in theory, but for removeView() the document
|
|
// might already be destroyed and removed. Also used as refcounter.
|
|
m_documents.insert(view, view->document());
|
|
}
|
|
|
|
void AutoBracePlugin::removeView(KTextEditor::View *view)
|
|
{
|
|
if (m_documents.contains(view))
|
|
{
|
|
KTextEditor::Document *document = m_documents.value(view);
|
|
m_documents.remove(view);
|
|
|
|
// Only detach from the document if it was the last view pointing to that.
|
|
if (m_documents.keys(document).empty()) {
|
|
AutoBracePluginDocument *docplugin = m_docplugins.value(document);
|
|
m_docplugins.remove(document);
|
|
delete docplugin;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AutoBracePlugin::readConfig()
|
|
{
|
|
KConfigGroup cg(KGlobal::config(), "AutoBrace Plugin");
|
|
m_autoBrackets = cg.readEntry("autobrackets", true);
|
|
m_autoQuotations = cg.readEntry("autoquotations", false);
|
|
}
|
|
|
|
void AutoBracePlugin::writeConfig()
|
|
{
|
|
KConfigGroup cg(KGlobal::config(), "AutoBrace Plugin");
|
|
cg.writeEntry("autobrackets", m_autoBrackets);
|
|
cg.writeEntry("autoquotations", m_autoQuotations);
|
|
}
|
|
|
|
/// AutoBracePluginDocument
|
|
|
|
AutoBracePluginDocument::AutoBracePluginDocument(KTextEditor::Document* document, const bool& autoBrackets, const bool& autoQuotations)
|
|
: QObject(document), m_insertionLine(0), m_withSemicolon(false),
|
|
m_lastRange(KTextEditor::Range::invalid()), m_autoBrackets(autoBrackets), m_autoQuotations(autoQuotations)
|
|
{
|
|
connect(document, SIGNAL(exclusiveEditStart(KTextEditor::Document*)),
|
|
this, SLOT(disconnectSlots(KTextEditor::Document*)));
|
|
connect(document, SIGNAL(exclusiveEditEnd(KTextEditor::Document*)),
|
|
this, SLOT(connectSlots(KTextEditor::Document*)));
|
|
|
|
connectSlots(document);
|
|
}
|
|
|
|
AutoBracePluginDocument::~AutoBracePluginDocument()
|
|
{
|
|
disconnect(parent() /* == document */, 0, this, 0);
|
|
}
|
|
|
|
/**
|
|
* (Re-)setups slots for AutoBracePluginDocument.
|
|
* @param document Current document.
|
|
*/
|
|
void AutoBracePluginDocument::connectSlots(KTextEditor::Document *document)
|
|
{
|
|
connect(document, SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)),
|
|
this, SLOT(slotTextInserted(KTextEditor::Document*,KTextEditor::Range)));
|
|
connect(document, SIGNAL(textRemoved(KTextEditor::Document*,KTextEditor::Range)),
|
|
this, SLOT(slotTextRemoved(KTextEditor::Document*,KTextEditor::Range)));
|
|
}
|
|
|
|
void AutoBracePluginDocument::disconnectSlots(KTextEditor::Document* document)
|
|
{
|
|
disconnect(document, SIGNAL(textInserted(KTextEditor::Document*,KTextEditor::Range)),
|
|
this, SLOT(slotTextInserted(KTextEditor::Document*,KTextEditor::Range)));
|
|
disconnect(document, SIGNAL(textRemoved(KTextEditor::Document*,KTextEditor::Range)),
|
|
this, SLOT(slotTextRemoved(KTextEditor::Document*,KTextEditor::Range)));
|
|
disconnect(document, SIGNAL(textChanged(KTextEditor::Document*)),
|
|
this, SLOT(slotTextChanged(KTextEditor::Document*)));
|
|
}
|
|
|
|
/**
|
|
* Connected to KTextEditor::Document::textChanged() once slotTextInserted()
|
|
* found a line with an opening brace. This takes care of inserting the new
|
|
* line with its closing counterpart.
|
|
*/
|
|
void AutoBracePluginDocument::slotTextChanged(KTextEditor::Document *document) {
|
|
// Disconnect from all signals as we insert stuff by ourselves.
|
|
// Prevent infinite recursion.
|
|
disconnectSlots(document);
|
|
|
|
// Make really sure that we want to insert the brace, paste guard and all.
|
|
if (m_insertionLine != 0
|
|
&& m_insertionLine == document->activeView()->cursorPosition().line()
|
|
&& document->line(m_insertionLine).trimmed().isEmpty())
|
|
{
|
|
KTextEditor::View *view = document->activeView();
|
|
document->startEditing();
|
|
|
|
// If the document's View is a KateView then it's able to indent.
|
|
// We hereby ignore the indenter and always indent correctly. (Sorry!)
|
|
if (view->inherits("KateView")) {
|
|
// Correctly indent the empty line. Magic!
|
|
KTextEditor::Range lineRange(
|
|
m_insertionLine, 0,
|
|
m_insertionLine, document->lineLength(m_insertionLine)
|
|
);
|
|
document->replaceText(lineRange, m_indentation);
|
|
|
|
connect(this, SIGNAL(indent()), view, SLOT(indent()));
|
|
emit indent();
|
|
disconnect(this, SIGNAL(indent()), view, SLOT(indent()));
|
|
}
|
|
// The line with the closing brace. (Inserted via insertLine() in order
|
|
// to avoid space removal by potential indenters.)
|
|
document->insertLine(m_insertionLine + 1, m_indentation + '}' + (m_withSemicolon ? ";" : ""));
|
|
|
|
document->endEditing();
|
|
view->setCursorPosition(document->endOfLine(m_insertionLine));
|
|
}
|
|
m_insertionLine = 0;
|
|
|
|
// Re-enable the textInserted() slot again.
|
|
connectSlots(document);
|
|
}
|
|
|
|
/**
|
|
* Connected to KTextEditor::Documet::textRemoved. Allows to delete
|
|
* an automatically inserted closing bracket if the opening counterpart
|
|
* has been removed.
|
|
*/
|
|
void AutoBracePluginDocument::slotTextRemoved(KTextEditor::Document* document, const KTextEditor::Range& range)
|
|
{
|
|
// If last range equals the deleted text range (last range
|
|
// is last inserted bracket), we also delete the associated closing bracket.
|
|
if (m_lastRange == range) {
|
|
// avoid endless recursion
|
|
disconnectSlots(document);
|
|
|
|
// Delete the character at the same range because the opening
|
|
// bracket has already been removed so the closing bracket
|
|
// should now have been shifted to that same position
|
|
if (range.isValid()) {
|
|
document->removeText(range);
|
|
}
|
|
|
|
connectSlots(document);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connected to KTextEditor::Document::textInserted(), which is emitted on all
|
|
* insertion changes. Line text and line breaks are emitted separately by
|
|
* KatePart, and pasted text gets the same treatment as manually entered text.
|
|
* Because of that, we discard paste operations by only remembering the
|
|
* insertion status for the last line that was entered.
|
|
*/
|
|
void AutoBracePluginDocument::slotTextInserted(KTextEditor::Document *document,
|
|
const KTextEditor::Range& range)
|
|
{
|
|
// Fill brackets map matching opening and closing brackets.
|
|
QMap<QString,QString> brackets;
|
|
brackets["("] = ")";
|
|
brackets["["] = "]";
|
|
|
|
// latex wants {, too
|
|
if (document->mode() == "LaTeX")
|
|
brackets["{"] = "}";
|
|
|
|
// List of Tokens after which an automatic bracket expanion
|
|
// is allowed.
|
|
const static QStringList allowedNextToken = QStringList() << "]" << ")" << ","
|
|
<< "." << ";" << "\n" << "\t" << " " << "";
|
|
const QString text = document->text(range);
|
|
|
|
// An insertion operation cancels any last range removal
|
|
// operation
|
|
m_lastRange = KTextEditor::Range::invalid();
|
|
|
|
// Make sure to handle only:
|
|
// 1.) New lines after { (brace openers)
|
|
// 2.) Opening braces like '(' and '['
|
|
// 3.) Quotation marks like " and '
|
|
|
|
// Handle brace openers
|
|
if (text == "\n") {
|
|
// Remember this position as insertion candidate.
|
|
// We don't directly insert this here because of KatePart specifics:
|
|
// a) Setting the cursor position crashes at this point, and
|
|
// b) textChanged() only gets called once per edit operation, so we can
|
|
// ignore the same braces when they're being inserted via paste.
|
|
if (isInsertionCandidate(document, range.start().line())) {
|
|
m_insertionLine = range.end().line();
|
|
connect(document, SIGNAL(textChanged(KTextEditor::Document*)),
|
|
this, SLOT(slotTextChanged(KTextEditor::Document*)));
|
|
}
|
|
else {
|
|
m_insertionLine = 0;
|
|
}
|
|
}
|
|
// Opening brackets (defined in ctor)
|
|
else if (m_autoBrackets && brackets.contains(text)) {
|
|
// Only insert auto closing brackets if current text range
|
|
// is followed by one of the allowed next tokens.
|
|
if (allowedNextToken.contains(nextToken(document,range))) {
|
|
insertAutoBracket(document, range, brackets[text]);
|
|
}
|
|
|
|
}
|
|
// Check whether closing brackets are allowed.
|
|
// If a brace is not allowed remove it
|
|
// and set the cursor to the position after that text range.
|
|
// Bracket tests bases on this simple idea: A bracket can only be inserted
|
|
// if it is NOT followed by the same bracket. This results in overwriting closing brackets.
|
|
else if (m_autoBrackets && brackets.values().contains(text)) {
|
|
if (nextToken(document,range) == text) {
|
|
KTextEditor::Cursor saved = range.end();
|
|
document->removeText(range);
|
|
document->activeView()->setCursorPosition(saved);
|
|
}
|
|
}
|
|
// Insert auto-quotation marks (if enabled). Same idea as with brackets
|
|
// applies here: double quotation marks are eaten up and only inserted if not
|
|
// followed by the same quoation mark. Additionally automatic quotation marks
|
|
// are inserted only if NOT followed by a back slash (escaping character).
|
|
else if (m_autoQuotations && (text == "\"" || text == "\'") && previousToken(document, range) != "\\") {
|
|
const QString next = nextToken(document, range);
|
|
// Eat it if already there
|
|
if (next == text) {
|
|
KTextEditor::Cursor saved = range.end();
|
|
document->removeText(range);
|
|
document->activeView()->setCursorPosition(saved);
|
|
}
|
|
// Quotation marks only inserted if followed by one of the allowed
|
|
// next tokens and the number of marks in the insertion line is even
|
|
// (excluding the already inserted mark)
|
|
else if (allowedNextToken.contains(next)
|
|
&& (document->line(range.start().line()).count(text) % 2) ) {
|
|
insertAutoBracket(document, range, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Automatically inserts closing bracket. Cursor
|
|
* is placed in between the brackets.
|
|
* @param document Current document.
|
|
* @param range Inserted text range (by text-inserted slot)
|
|
* @param brace Brace to insert
|
|
*/
|
|
void AutoBracePluginDocument::insertAutoBracket(KTextEditor::Document *document,
|
|
const KTextEditor::Range& range,
|
|
const QString& brace) {
|
|
// Disconnect Slots to avoid check for redundant closing brackets
|
|
disconnectSlots(document);
|
|
|
|
// Save range to allow following remove operation to
|
|
// detect the corresponding closing bracket
|
|
m_lastRange = range;
|
|
|
|
KTextEditor::Cursor saved = range.end();
|
|
// Depending on brace, insert corresponding closing brace.
|
|
document->insertText(range.end(), brace);
|
|
document->activeView()->setCursorPosition(saved);
|
|
|
|
// Re-Enable insertion slot.
|
|
connectSlots(document);
|
|
}
|
|
|
|
/**
|
|
* Returns next character after specified text range in document.
|
|
* @param document Current document.
|
|
* @param range Inserted text range (by text-inserted slot)
|
|
* @return Next character after text range
|
|
*/
|
|
const QString AutoBracePluginDocument::nextToken(KTextEditor::Document* document, const KTextEditor::Range& range)
|
|
{
|
|
// Calculate range after insertion (exactly one character)
|
|
KTextEditor::Range afterRange(range.end(), range.end().line(), range.end().column()+1);
|
|
|
|
return (afterRange.isValid() ? document->text(afterRange) : "");
|
|
}
|
|
|
|
/**
|
|
* Returns previous character before specified text range in document.
|
|
* @param document Current document.
|
|
* @param range Inserted text range (by text-inserted slot)
|
|
* @return Next character after text range
|
|
*/
|
|
const QString AutoBracePluginDocument::previousToken(KTextEditor::Document* document, const KTextEditor::Range& range)
|
|
{
|
|
// Calculate range before insertion (exactly one character)
|
|
KTextEditor::Range beforeRange(range.start().line(), range.start().column()-1, range.start().line(),
|
|
range.start().column());
|
|
|
|
return (beforeRange.isValid() ? document->text(beforeRange) : "");
|
|
}
|
|
|
|
bool AutoBracePluginDocument::isInsertionCandidate(KTextEditor::Document *document, int openingBraceLine) {
|
|
QString line = document->line(openingBraceLine);
|
|
if (line.isEmpty() || !line.endsWith('{')) {
|
|
return false;
|
|
}
|
|
|
|
// Get the indentation prefix.
|
|
QRegExp rx("^(\\s+)");
|
|
QString indentation = (rx.indexIn(line) == -1) ? "" : rx.cap(1);
|
|
|
|
// Determine whether to insert a brace or not, depending on the indentation
|
|
// of the upcoming (non-empty) line.
|
|
bool isCandidate = true;
|
|
QString indentationLength = QString::number(indentation.length());
|
|
QString indentationLengthMinusOne = QString::number(indentation.length() - 1);
|
|
|
|
///TODO: make configurable
|
|
// these tokens must not start a line that is used to get the correct indendation width
|
|
QStringList forbiddenTokenList;
|
|
if ( line.contains("class") || line.contains("interface") || line.contains("struct") ) {
|
|
forbiddenTokenList << "private" << "public" << "protected";
|
|
if ( document->mode() == "C++" ) {
|
|
forbiddenTokenList << "signals" << "Q_SIGNALS";
|
|
} else {
|
|
// PHP and potentially others
|
|
forbiddenTokenList << "function";
|
|
}
|
|
}
|
|
if ( (document->mode() == "C++" || document->mode() == "C") && line.contains("namespace", Qt::CaseInsensitive) ) {
|
|
// C++ specific
|
|
forbiddenTokenList << "class" << "struct";
|
|
}
|
|
const QString forbiddenTokens = forbiddenTokenList.isEmpty() ? QLatin1String("") : QString(QLatin1String("(?!") + forbiddenTokenList.join(QLatin1String("|")) + QLatin1Char(')'));
|
|
|
|
for (int i = openingBraceLine + 1; i < document->lines(); ++i)
|
|
{
|
|
line = document->line(i);
|
|
if (line.trimmed().isEmpty()) {
|
|
continue; // Empty lines are not a reliable source of information.
|
|
}
|
|
|
|
if (indentation.length() == 0) {
|
|
// Inserting a brace is ok if there is a line (not starting with a
|
|
// brace) without indentation.
|
|
rx.setPattern("^(?=[^\\}\\s])"
|
|
// But it's not OK if the line starts with one of our forbidden tokens.
|
|
+ forbiddenTokens
|
|
);
|
|
}
|
|
else {
|
|
rx.setPattern("^(?:"
|
|
// Inserting a brace is ok if there is a closing brace with
|
|
// less indentation than the opener line.
|
|
"[\\s]{0," + indentationLengthMinusOne + "}\\}"
|
|
"|"
|
|
// Inserting a brace is ok if there is a line (not starting with a
|
|
// brace) with less or similar indentation as the original line.
|
|
"[\\s]{0," + indentationLength + "}(?=[^\\}\\s])"
|
|
// But it's not OK if the line starts with one of our forbidden tokens.
|
|
+ forbiddenTokens +
|
|
")"
|
|
);
|
|
}
|
|
|
|
if (rx.indexIn(line) == -1) {
|
|
// There is already a brace, or the line is indented more than the
|
|
// opener line (which means we expect a brace somewhere further down),
|
|
// or we found a forbidden token.
|
|
// So don't insert the brace, and just indent the line.
|
|
isCandidate = false;
|
|
}
|
|
// Quit the loop - a non-empty line always leads to a definitive decision.
|
|
break;
|
|
}
|
|
|
|
if (isCandidate) {
|
|
m_indentation = indentation;
|
|
// in C++ automatically add a semicolon after the closing brace when we create a new class/struct
|
|
if ( (document->mode() == "C++" || document->mode() == "C")
|
|
&& document->line(openingBraceLine).indexOf(QRegExp("(?:class|struct|enum)\\s+[^\\s]+(\\s*[:,](\\s*((public|protected|private)\\s+)?[^\\s]+))*\\s*\\{\\s*$")) != -1 )
|
|
{
|
|
m_withSemicolon = true;
|
|
} else {
|
|
m_withSemicolon = false;
|
|
}
|
|
}
|
|
return isCandidate;
|
|
}
|
|
|
|
#include "moc_autobrace.cpp"
|