kde-workspace/kate/addons/ktexteditor/autobrace/autobrace.cpp
Ivailo Monev f68295ea28 generic: move sub-projects from kde-baseapps [ci reset]
Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2022-05-14 21:56:54 +03:00

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"