var katescript = {
    "name": "C Style",
    "author": "Dominik Haumann <dhdev@gmx.de>, Milian Wolff <mail@milianw.de>",
    "license": "LGPL",
    "revision": 6,
    "kate-version": "5.1"
}; // kate-script-header, must be at the start of the file without comments, pure json

/*
    This file is part of the Kate Project.

    SPDX-License-Identifier: LGPL-2.0-only
*/

// required katepart js libraries
require ("range.js");
require ("string.js");
require ("utils.js");

//BEGIN USER CONFIGURATION
var cfgIndentCase = true;         // indent 'case' and 'default' in a switch?
var cfgIndentNamespace = true;    // indent after 'namespace'?
var cfgAutoInsertStar = true;    // auto insert '*' in C-comments
var cfgSnapSlash = true;         // snap '/' to '*/' in C-comments
var cfgAutoInsertSlashes = false; // auto insert '//' after C++-comments
var cfgAccessModifiers = 0;       // indent level of access modifiers, relative to the class indent level
                                  // set to -1 to disable auto-indendation after access modifiers.
//END USER CONFIGURATION

// indent gets three arguments: line, indentwidth in spaces, typed character
// indent

// specifies the characters which should trigger indent, beside the default '\n'
triggerCharacters = "{})/:;#";

var debugMode = false;

function dbg() {
    if (debugMode) {
        debug.apply(this, arguments);
    }
}

const multiLineStringLiteralStartRegex = /R"(.+)?\(/

//BEGIN global variables and functions
// maximum number of lines we look backwards/forward to find out the indentation
// level (the bigger the number, the longer might be the delay)
var gLineDelimiter = 50;     // number

var gIndentWidth = 4;
var gMode = "C";
//END global variables and functions


/**
 * Search for a corresponding '{' and return its indentation level. If not found
 * return null; (line/column) are the start of the search.
 */
function findLeftBrace(line, column)
{
    var cursor = document.anchor(line, column, '{');
    if (cursor.isValid()) {
        var parenthesisCursor = tryParenthesisBeforeBrace(cursor.line, cursor.column);
        if (parenthesisCursor.isValid())
            cursor = parenthesisCursor;

        dbg("findLeftBrace: success in line " + cursor.line);
        return document.firstVirtualColumn(cursor.line);
    }

    return -1;
}

/**
 * Find last non-empty line that is not inside a comment or preprocessor
 */
function lastNonEmptyLine(line)
{
    while (true) {
        line = document.prevNonEmptyLine(line);
        if ( line == -1 ) {
            return -1;
        }
        var string = document.line(line).ltrim();
        ///TODO: cpp multiline comments
        ///TODO: multiline macros
        if ( string.startsWith("//") || string.startsWith('#') ) {
            --line;
            continue;
        }
        break;
    }

    return line;
}

/**
 * Character at (line, column) has to be a '{'.
 * Now try to find the right line for indentation for constructs like:
 * if (a == b
 *     && c == d) { <- check for ')', and find '(', then return its indentation
 * Returns null, if no success, otherwise an object {line, column}
 */
function tryParenthesisBeforeBrace(line, column)
{
    var firstColumn = document.firstColumn(line);
    while (column > firstColumn && document.isSpace(line, --column));
    if (document.charAt(line, column) == ')')
        return document.anchor(line, column, '(');
    return Cursor.invalid();
}

/**
 * Check for default and case keywords and assume we are in a switch statement.
 * Try to find a previous default, case or switch and return its indentation or
 * -1 if not found.
 */
function trySwitchStatement(line)
{
    var currentString = document.line(line);
    if (currentString.search(/^\s*(default\s*|case\b.*):/) == -1)
        return -1;

    var indentation = -1;
    var lineDelimiter = gLineDelimiter;
    var currentLine = line;

    while (currentLine > 0 && lineDelimiter > 0) {
        --currentLine;
        --lineDelimiter;
        if (document.firstColumn(currentLine) == -1)
            continue;

        currentString = document.line(currentLine);
        if (currentString.search(/^\s*(default\s*|case\b.*):/) != -1) {
            indentation = document.firstVirtualColumn(currentLine);
            break;
        } else if (currentString.search(/^\s*switch\b/) != -1) {
            indentation = document.firstVirtualColumn(currentLine);
            if (cfgIndentCase)
                indentation += gIndentWidth;
            break;
        }
    }

    if (indentation != -1) dbg("trySwitchStatement: success in line " + currentLine);
    return indentation;
}

/**
 * Check for private, protected, public, signals etc... and assume we are in a
 * class definition. Try to find a previous private/protected/private... or
 * class and return its indentation or null if not found.
 */
function tryAccessModifiers(line)
{
    if (cfgAccessModifiers < 0)
        return -1;

    var currentString = document.line(line);
    if (currentString.search(/^\s*((public|protected|private)\s*(slots|Q_SLOTS)?|(signals|Q_SIGNALS)\s*):\s*$/) == -1)
        return -1;

    var cursor = document.anchor(line, 0, '{');
    if (!cursor.isValid())
        return -1;

    var indentation = document.firstVirtualColumn(cursor.line);
    for ( var i = 0; i < cfgAccessModifiers; ++i ) {
        indentation += gIndentWidth;
    }

    if (indentation != -1) dbg("tryAccessModifiers: success in line " + cursor.line);
    return indentation;
}



/**
 * Get the position of the first non-space character
 * return: position or -1, if there is no non-space character
 */
function firstNonSpace(text)
{
    if (text && text.search(/^(\s*)\S/) != -1)
        return RegExp.$1.length;

    return -1;
}

/**
 * Get the position of the last non-space character
 * return: position or -1, if there is no non-space character
 */
function lastNonSpace(text)
{
    if (text && text.search(/(.*)\S\s*$/) != -1)
        return RegExp.$1.length;

    return -1;
}

/**
 * Don't indent when in a multiline string literal of form "something\
nextline"
 * Note: the function does not check for comments
 */
function tryString(line)
{
    var currentLine = line;
    var currentString;

    // go line up as long as the previous line ends with an escape character '\'
    while (currentLine >= 0) {
        currentString = document.line(currentLine - 1);
        if (currentString.charAt(document.lastColumn(currentLine - 1)) != '\\')
            break;
        --currentLine;
    }

    // iterate through all lines and toggle bool insideString for every quote
    var insideString = false;
    var indentation = -1;
    while (currentLine < line) {
        currentString = document.line(currentLine);
        var char1;
        var i;
        var lineLength = document.lineLength(currentLine);
        for (i = 0; i < lineLength; ++i) {
            char1 = currentString.charAt(i);
            if (char1 == "\\") {
                // skip escaped character
                ++i;
            } else if (char1 == "\"") {
                insideString = !insideString;
            }
        }
        ++currentLine;
    }

    return insideString ? 0 : -1;
}

/**
 * C comment checking. If the previous line begins with a "/*" or a "* ", then
 * return its leading white spaces + ' *' + the white spaces after the *
 * return: filler string or null, if not in a C comment
 */
function tryCComment(line)
{
    var currentLine = document.prevNonEmptyLine(line - 1);
    if (currentLine < 0)
        return -1;

    var indentation = -1;

    // we found a */, search the opening /* and return its indentation level
    if (document.endsWith(currentLine, "*/", true)) {
        var cursor = document.rfind(currentLine, document.lastColumn(currentLine), "/*");
        if (cursor.isValid() && cursor.column == document.firstColumn(cursor.line))
            indentation = document.firstVirtualColumn(cursor.line);

        if (indentation != -1) dbg("tryCComment: success (1) in line " + cursor.line);
        return indentation;
    }

    // inbetween was an empty line, so do not copy the "*" character
    if (currentLine != line - 1)
        return -1;

    var firstPos = document.firstColumn(currentLine);
    var lastPos = document.lastColumn(currentLine);
    var char1 = document.charAt(currentLine, firstPos);
    var char2 = document.charAt(currentLine, firstPos + 1);
    var currentString = document.line(currentLine);

    if (char1 == '/' && char2 == '*' && !currentString.contains("*/")) {
        indentation = document.firstVirtualColumn(currentLine);
        if (cfgAutoInsertStar) {
            // only add '*', if there is none yet.
            indentation += 1;
            if (document.firstChar(line) != '*')
                document.insertText(line, view.cursorPosition().column, '*');
            if (!document.isSpace(line, document.firstColumn(line) + 1) && !document.endsWith(line, "*/", true))
                document.insertText(line, document.firstColumn(line) + 1, ' ');
        }
    } else if (char1 == '*') {
        var commentLine = currentLine;
        while (commentLine >= 0 && document.firstChar(commentLine) == '*'
            && !document.endsWith(commentLine, "*/", true)
        ) {
            --commentLine;
        }
        if (commentLine < 0) {
            indentation = document.firstVirtualColumn(currentLine);
        } else if (document.startsWith(commentLine, "/*", true) && !document.endsWith(commentLine, "*/", true)) {
            // found a /*, and all succeeding lines start with a *, so it's a comment block
            indentation = document.firstVirtualColumn(currentLine);

            // only add '*', if there is none yet.
            if (cfgAutoInsertStar && document.firstChar(line) != '*') {
                document.insertText(line, view.cursorPosition().column, '*');
                if (!document.isSpace(line, document.firstColumn(line) + 1))
                    document.insertText(line, document.firstColumn(line) + 1, ' ');
            }
        }
    }

    if (indentation != -1) dbg("tryCComment: success (2) in line " + currentLine);
    return indentation;
}

/**
 * C++ comment checking. when we want to insert slashes:
 * //, ///, //! ///<, //!< and ////...
 * return: filler string or null, if not in a star comment
 * NOTE: otherwise comments get skipped generally and we use the last code-line
 */
function tryCppComment(line)
{
    var currentLine = line - 1;
    if (currentLine < 0 || !cfgAutoInsertSlashes)
        return -1;

    var indentation = -1;
    var comment = document.startsWith(currentLine, "//", true);

    // allowed are: //, ///, //! ///<, //!< and ////...
    if (comment) {
        var firstPos = document.firstColumn(currentLine);
        var currentString = document.line(currentLine);

        var char3 = currentString.charAt(firstPos + 2);
        var char4 = currentString.charAt(firstPos + 3);
        indentation = document.firstVirtualColumn(currentLine);

        if (cfgAutoInsertSlashes) {
            if (char3 == '/' && char4 == '/') {
                // match ////... and replace by only two: //
                currentString.search(/^\s*(\/\/)/);
            } else if (char3 == '/' || char3 == '!') {
                // match ///, //!, ///< and //!
                currentString.search(/^\s*(\/\/[\/!][<]?\s*)/);
            } else {
                // only //, nothing else
                currentString.search(/^\s*(\/\/\s*)/);
            }
            document.insertText(line, view.cursorPosition().column, RegExp.$1);
        }
    }

    if (indentation != -1) dbg("tryCppComment: success in line " + currentLine);
    return indentation;
}

/**
 * If the last non-empty line ends with a { or [, take its indentation level (or
 * maybe the one found by tryParenthesisBeforeBrace()) and return it increased
 * by 1 indetation level (special case: namespaces indentation depends on
 * cfgIndentNamespace). If not found, return null.
 */
function tryBrace(line)
{
    function isNamespace(line, column)
    {
        if (document.firstColumn(line) == column && line > 0)
            --line;
        var currentString = document.line(line);
        return (currentString.search(/^\s*namespace\b/) != -1);
    }

    var currentLine = lastNonEmptyLine(line - 1);
    if (currentLine < 0)
        return -1;

    var lastPos = document.lastColumn(currentLine);
    var indentation = -1;

    var currentString = document.line(currentLine);
    var matchColumn = currentString.search(/\{[^\}]*$/);
    // line ends with [, e.g., array in js or dart
    if (matchColumn == -1 && currentString.endsWith("[")) {
        matchColumn = currentString.length - 1;
    }

    if (matchColumn != -1 && document.isCode(currentLine, matchColumn)) {
        dbg("tryBrace: Closing bracket in line " + currentLine);
        var cursor = tryParenthesisBeforeBrace(currentLine, lastPos);
        if (cursor.isValid()) {
            indentation = document.firstVirtualColumn(cursor.line) + gIndentWidth;
        } else {
            indentation = document.firstVirtualColumn(currentLine);
            if (cfgIndentNamespace || !isNamespace(currentLine, lastPos))
                // take its indentation and add one indentation level
                indentation += gIndentWidth;
        }
    }

    if (indentation != -1) dbg("tryBrace: success in line " + currentLine);
    return indentation;
}

/**
 * Check for if, else, while, do, switch, private, public, protected, signals,
 * default, case etc... keywords, as we want to indent then. If   is
 * non-null/true, then indentation is not increased.
 * Note: The code is written to be called *after* tryCComment and tryCppComment!
 */
function tryCKeywords(line, isBrace)
{
    var currentLine = lastNonEmptyLine(line - 1);
    if (currentLine < 0)
        return -1;

    // if line ends with ')', find the '(' and check this line then.
    var lastPos = document.lastColumn(currentLine);
    var cursor = Cursor.invalid();
    if (document.charAt(currentLine, lastPos) == ')')
        cursor = document.anchor(currentLine, lastPos, '(');
    if (cursor.isValid())
        currentLine = cursor.line;

    // found non-empty line
    var currentString = document.line(currentLine);
    if (currentString.search(/^\s*(if\b|for(each)?|do\b|while|switch|[}]?\s*else(if)?|((private|public|protected|case|default|signals|Q_SIGNALS).*:))/) == -1)
        return -1;
    dbg("Found first word: " + RegExp.$1);
    lastPos = document.lastColumn(currentLine);
    var lastChar = currentString.charAt(lastPos);
    var indentation = -1;

    // ignore trailing comments see: https://bugs.kde.org/show_bug.cgi?id=189339
    var commentPos = currentString.indexOf("//")
    if (commentPos != -1) {
        currentString = currentString.substring(0, commentPos).rtrim();
        lastChar = currentString.charAt(currentString.length - 1);
    }

    // try to ignore lines like: if (a) b; or if (a) { b; }
    if (lastChar != ';' && lastChar != '}') {
        // take its indentation and add one indentation level
        indentation = document.firstVirtualColumn(currentLine);
        if (!isBrace)
            indentation += gIndentWidth;
    } else if (lastChar == ';') {
        // stuff like:
        // for(int b;
        //     b < 10;
        //     --b)
        cursor = document.anchor(currentLine, lastPos, '(');
        if (cursor.isValid()) {
            // Same line, we know there is a keyword here
            if (cursor.line == line) {
                indentation = document.toVirtualColumn(cursor.line, cursor.column + 1);
            } else {
                // check that the returned cursor's line contains
                // a keyword and isn't a func call.
                var cursorLine = document.line(cursor.line);
                var s = cursorLine.search(/^\s*(if\b|for(each)?|do\b|while|switch|[}]?\s*else(if)?)/);
                if (s != -1) {
                    indentation = document.toVirtualColumn(cursor.line, cursor.column + 1);
                }
            }
        }
    }

    if (indentation != -1) dbg("tryCKeywords: success in line " + currentLine);
    return indentation;
}

/**
 * Is this line a single statement of a condition:
 * if (true)
 *     doWork();
 * - Finds the previous non empty line
 * - Checks if it has control keyword
 * - Checks if it doesn't have brace
 */
function isSingleStmtCondition(line)
{
    if (line > 1) {
        var prev = lastNonEmptyLine(line - 1);
        var prevText = document.line(prev);
        var hasKeyword = prevText.search(/^\s*(if\b|[}]?\s*else(if)?\b|do\b|while\b|for(each)?\b)/);
        if (hasKeyword != -1 && !prevText.contains("{")) {
            return true;
        }
    }
    return false;
}

/**
 * Search for if, do, while, for, ... as we want to indent then.
 * Return null, if nothing useful found.
 * Note: The code is written to be called *after* tryCComment and tryCppComment!
 */
function tryCondition(line)
{
    var currentLine = lastNonEmptyLine(line - 1);
    if (currentLine < 0)
        return -1;

    // found non-empty line
    var currentString = document.line(currentLine);
    var lastPos = document.lastColumn(currentLine);
    var lastChar = currentString.charAt(lastPos);
    var indentation = -1;

    if (lastChar == ';'
        && currentString.search(/^\s*(if\b|[}]?\s*else(if)?\b|do\b|while\b|for(each)?\b)/) == -1)
    {
        // idea: we had something like:
        //   if/while/for (expression)
        //       statement();  <-- we catch this trailing ';'
        // Now, look for a line that starts with if/for/while, that has one
        // indent level less.
        var currentIndentation = document.firstVirtualColumn(currentLine);
        if (currentIndentation == 0)
            return -1;

        // Is this a condition with only one stmt in it
        var singleStmt = isSingleStmtCondition(currentLine);
        if (singleStmt) {
            var leftBrace = findLeftBrace(currentLine, 0);
            if (leftBrace != -1) {
                indentation = leftBrace + gIndentWidth;
                dbg("tryCondition(1): success in line " + currentLine);
                return indentation;
            }
        }


        var lineDelimiter = 10; // 10 limit search, hope this is a sane value
        while (currentLine > 0 && lineDelimiter > 0) {
            --currentLine;
            --lineDelimiter;
            var firstPosVirtual = document.firstVirtualColumn(currentLine);
            if (firstPosVirtual == -1)
                continue;

            if (firstPosVirtual < currentIndentation) {
                currentString = document.line(currentLine);
                if (currentString.search(/^\s*(if\b|[}]?\s*else(if)?|do\b|while\b|for(each)?)[^{]*$/) != -1)
                    indentation = firstPosVirtual;
                break;
            } else if (currentLine == 0 || lineDelimiter == 0) {
                return indentation;
            }
        }
    }

    if (indentation != -1) dbg("tryCondition(2): success in line " + currentLine);
    return indentation;
}

/**
 * If the non-empty line ends with ); or ',', then search for '(' and return its
 * indentation; also try to ignore trailing comments.
 */
function tryStatement(line)
{
    var currentLine = lastNonEmptyLine(line - 1);
    if (currentLine < 0)
        return -1;

    var indentation = -1;
    var currentString = document.line(currentLine);
    if (currentString.endsWith('(')) {
        // increase indent level
        indentation = document.firstVirtualColumn(currentLine) + gIndentWidth;
        dbg("tryStatement (1): success in line " + currentLine);
        return indentation;
    }
    var alignOnSingleQuote = gMode == "PHP/PHP" || gMode == "JavaScript";
    // align on strings "..."\n => below the opening quote
    // multi-language support: [\.+] for javascript or php
    var result = /^(.*)(,|"|'|\))(;?)\s*[\.+]?\s*(\/\/.*|\/\*.*\*\/\s*)?$/.exec(currentString);
    if (result != null && result.index == 0) {
        var alignOnAnchor = result[3].length == 0 && result[2] != ')';
        // search for opening ", ' or (
        var cursor = Cursor.invalid();
        if (result[2] == '"' || (alignOnSingleQuote && result[2] == "'")) {

            while(true) {
                var i = result[1].length - 1; // start from matched closing ' or "
                // find string opener
                for ( ; i >= 0; --i ) {
                    // make sure it's not commented out
                    if (currentString[i] == result[2] && (i == 0 || currentString[i - 1] != '\\')) {
                        // also make sure that this is not a line like '#include "..."' <-- we don't want to indent here
                        if (currentString.match(/^#include/) || currentString.match(/'use strict'/)) {
                            return indentation;
                        }
                        cursor = new Cursor(currentLine, i);
                        break;
                    }
                }
                if (!alignOnAnchor && currentLine) {
                    // when we finished the statement (;) we need to get the first line and use it's indentation
                    // i.e.: $foo = "asdf"; -> align on $
                    --i; // skip " or '
                    // skip whitespaces and stuff like + or . (for PHP, JavaScript, ...)
                    for ( ; i >= 0; --i ) {
                        if (currentString[i] == ' ' || currentString[i] == "\t"
                            || currentString[i] == '.' || currentString[i] == '+')
                        {
                            continue;
                        } else {
                            break;
                        }
                    }
                    if ( i > 0 ) {
                        // there's something in this line, use it's indentation
                        break;
                    } else {
                        // go to previous line
                        --currentLine;
                        currentString = document.line(currentLine);
                    }
                } else {
                    break;
                }
            }
        } else if (result[2] == ',' && !currentString.match(/\(/)) {
            // assume a function call: check for '(' brace
            // - if not found, use previous indentation
            // - if found, compare the indentation depth of current line and open brace line
            //   - if current indentation depth is smaller, use that
            //   - otherwise, use the '(' indentation + following white spaces
            var currentIndentation = document.firstVirtualColumn(currentLine);
            var braceCursor = document.anchor(currentLine, result[1].length, '(');

            if (!braceCursor.isValid() || currentIndentation < braceCursor.column)
                indentation = currentIndentation;
            else {
                indentation = braceCursor.column + 1;
                while (document.isSpace(braceCursor.line, indentation))
                    ++indentation;
            }
        } else if (gMode == "Dart" && result[2] == ',' && currentString.contains("(") && currentString.contains(")")) {
            // Dart has a lot of stuff and function calls inside ctors
            // and code is not aligned at the opening paren hence:
            // do nothing
        } else {
            cursor = document.anchor(currentLine, result[1].length, '(');
        }
        if (cursor.isValid()) {
            if (alignOnAnchor) {
                currentLine = cursor.line;
                var column = cursor.column;
                var inc = 0;
                if (result[2] != '"' && result[2] != "'") {
                    // place one column after the opening parens
                    column++;
                    inc = 1;
                }
                var lastColumn = document.lastColumn(currentLine);
                while (column < lastColumn && document.isSpace(currentLine, column)) {
                    ++column;
                    inc = 1;
                }
                if (inc > 0)
                    indentation = document.toVirtualColumn(currentLine, column);
                else
                    indentation = document.firstVirtualColumn(currentLine);
            } else {
                currentLine = cursor.line;
                indentation = document.firstVirtualColumn(currentLine);
            }
        }
    } else if ( currentString.rtrim().endsWith(';') ) {
        indentation = document.firstVirtualColumn(currentLine);
    }

    if (indentation != -1) dbg("tryStatement (2): success in line " + currentLine);
    return indentation;
}

/**
 * find out whether we pressed return in something like {} or () or [] and indent properly:
 * {}
 * becomes:
 * {
 *   |
 * }
 */
function tryMatchedAnchor(line, alignOnly)
{
    var char = document.firstChar(line);
    if ( char != '}' && char != ')' && char != ']' ) {
        return -1;
    }
    // we pressed enter in e.g. ()
    var closingAnchor = document.anchor(line, 0, document.firstChar(line));
    if (!closingAnchor.isValid()) {
        // nothing found, continue with other cases
        return -1;
    }
    if (alignOnly) {
        // when aligning only, don't be too smart and just take the indent level of the open anchor
        return document.firstVirtualColumn(closingAnchor.line);
    }
    var lastChar = document.lastChar(line - 1);
    var charsMatch = ( lastChar == '(' && char == ')' ) ||
                     ( lastChar == '{' && char == '}' ) ||
                     ( lastChar == '[' && char == ']' );
    var indentLine = -1;
    var indentation = -1;
    if ( !charsMatch && char != '}' ) {
        // otherwise check whether the last line has the expected
        // indentation, if not use it instead and place the closing
        // anchor on the level of the openeing anchor
        var expectedIndentation = document.firstVirtualColumn(closingAnchor.line) + gIndentWidth;
        var actualIndentation = document.firstVirtualColumn(line - 1);
        var indentation = -1;
        if ( expectedIndentation <= actualIndentation ) {
            if ( lastChar == ',' && char != ')' ) {
                // use indentation of last line instead and place closing anchor
                // in same column of the openeing anchor
                document.insertText(line, document.firstColumn(line), "\n");
                view.setCursorPosition(line, actualIndentation);
                // indent closing anchor
                document.indent(new Range(line + 1, 0, line + 1, 1), document.toVirtualColumn(closingAnchor) / gIndentWidth);
                // make sure we add spaces to align perfectly on closing anchor
                var padding = document.toVirtualColumn(closingAnchor) % gIndentWidth;
                if ( padding > 0 ) {
                    document.insertText(line + 1, document.column - padding, String().fill(' ', padding));
                }
                indentation = actualIndentation;
            } else if ( expectedIndentation == actualIndentation ) {
                // otherwise don't add a new line, just use indentation of closing anchor line
                indentation = document.firstVirtualColumn(closingAnchor.line);
            } else {
                // otherwise don't add a new line, just align on closing anchor
                indentation = document.toVirtualColumn(closingAnchor) + 1;
            }
            dbg("tryMatchedAnchor: success in line " + closingAnchor.line);
            return indentation;
        } else if ( lastChar == ',') {
            dbg("tryMatchedAnchor: success in line " + closingAnchor.line);
            return document.toVirtualColumn(closingAnchor) + 1;
        }
    }

    // otherwise we i.e. pressed enter between (), [] or when we enter before curly brace
    // increase indentation and place closing anchor on the next line
    indentation = document.firstVirtualColumn(closingAnchor.line);
    document.insertText(line, document.firstColumn(line), "\n");
    view.setCursorPosition(line, indentation);
    // indent closing brace
    document.indent(new Range(line + 1, 0, line + 1, 1), indentation / gIndentWidth);
    dbg("tryMatchedAnchor: success in line " + closingAnchor.line);
    return indentation + gIndentWidth;
}

function escapeForRegex(string) {
    if (string !== null) {
        return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    }
    return string;
}

/**
 * Don't indent on multiline string literals of form R"[optional delimiter text]()[optional delimiter text]"
 */
function tryMultiLineStringLiteral(line) {
    // The algorithm in this function is only valid for C++, don't waste time for other languages
    if (!gMode.contains("C++")) {
        return -1;
    }
    var currentLine = line;
    var currentString;
    var delim = "";
    var found = false;

    var limit = 0; // ensure we don't search the whole document all the time
    // go line up as long as the previous line ends with an escape character '\'
    // go line up until we find something of form R"[delim](
    while (currentLine >= 0) {
        currentString = document.line(currentLine - 1);
        var startMatch = currentString.match(multiLineStringLiteralStartRegex);
        if (startMatch !== null) {
            found = true;
            delim = startMatch[1];
            break;
        }
        --currentLine;
        if (limit >= 25) {
            break;
        }
        limit++;
    }

    if (!found) {
        return -1;
    }

    // iterate through all lines and toggle bool insideString for every quote
    var insideString = true;
    var endReached = false;
    var indentation = null;
    const multiLineStringLiteralEndRegex = new RegExp('\\)' + escapeForRegex(delim) + '"');
    while (currentLine < line) {
        currentString = document.line(currentLine);
        dbg(currentString);
        var endMatch = currentString.match(multiLineStringLiteralEndRegex);
        if (endMatch !== null) {
            dbg("End reached: " + currentString);
            endReached = true;
            break;
        }
        ++currentLine;
    }
    indentation = document.firstVirtualColumn(line);
    dbg(endReached ? -1 : indentation);
    return endReached ? -1 : indentation;
}

/**
 * Indent line.
 * Return filler or null.
 */
function indentLine(line, alignOnly)
{
    var firstChar = document.firstChar(line);
    var lastChar = document.lastChar(line);

    var filler = -1;

    if (filler == -1)
        filler = tryMatchedAnchor(line, alignOnly);
    if (filler == -1)
        filler = tryCComment(line);
    if (filler == -1 && !alignOnly)
        filler = tryCppComment(line);
    if (filler == -1)
        filler = tryMultiLineStringLiteral(line);
    if (filler == -1)
        filler = tryString(line);
    if (filler == -1)
        filler = trySwitchStatement(line);
    if (filler == -1)
        filler = tryAccessModifiers(line);
    if (filler == -1)
        filler = tryBrace(line);
    if (filler == -1)
        filler = tryCKeywords(line, firstChar == '{');
    if (filler == -1)
        filler = tryCondition(line);
    if (filler == -1)
        filler = tryStatement(line);

    return filler;
}

function processChar(line, c)
{
    if (c == ';' || !triggerCharacters.contains(c))
        return -2;

    var cursor = view.cursorPosition();
    if (!cursor)
        return -2;

    var column = cursor.column;
    var firstPos = document.firstColumn(line);
    var prevFirstPos = document.firstColumn(line - 1);
    var lastPos = document.lastColumn(line);

    dbg("firstPos: " + firstPos);
    dbg("column..: " + column);
    dbg("char    : " + c);

    if (firstPos == column - 1 && c == '{') {
        // todo: maybe look for if etc.
        var filler = tryBrace(line);
        if (filler == -1)
            filler = tryCKeywords(line, true);
        if (filler == -1)
            filler = tryCComment(line); // checks, whether we had a "*/"
        if (filler == -1)
            filler = tryStatement(line);
        if (filler == -1)
            filler = -2;

        return filler;
    } else if (firstPos == column - 1 && c == '}' && document.charAt(line, column - 1) == '}') {
        // unindent after closing brace, but not when brace is auto inserted (i.e., behind cursor)
        var indentation = findLeftBrace(line, firstPos);
        if (indentation == -1)
            indentation = -2;
        return indentation;
    } else if (firstPos == column - 1 && c == '}'  && firstPos > prevFirstPos) {
        // align indentation to previous line when creating new block with auto brackets enabled
        // prevents over-indentation for if blocks and loops
        return document.toVirtualColumn(line - 1, prevFirstPos);
    } else if (cfgSnapSlash && c == '/' && lastPos == column - 1) {
        // try to snap the string "* /" to "*/"
        var currentString = document.line(line);
        if (/^(\s*)\*\s+\/\s*$/.test(currentString)) {
            currentString = RegExp.$1 + "*/";
            document.editBegin();
            document.removeLine(line);
            document.insertLine(line, currentString);
            view.setCursorPosition(line, currentString.length);
            document.editEnd();
        }
        return -2;
    } else if (c == ':') {
        // todo: handle case, default, signals, private, public, protected, Q_SIGNALS
        var filler = trySwitchStatement(line);
        if (filler == -1)
            filler = tryAccessModifiers(line);
        if (filler == -1)
            filler = -2;
        return filler;
    } else if (c == ')' && firstPos == column - 1) {
        // align on start of identifier of function call
        var openParen = document.anchor(line, column - 1, '(');
        if (openParen.isValid()) {
            // get identifier
            var callLine = document.line(openParen.line);
            // strip starting from opening paren
            callLine = callLine.substring(0, openParen.column - 1);
            indentation = callLine.search(/\b(\w+)\s*$/);
            if (indentation != -1) {
                return document.toVirtualColumn(openParen.line, indentation);
            }
        }
    } else if (firstPos == column - 1 && c == '#' && ( gMode == 'C' || gMode == 'C++' || gMode == 'ISO C++' ) ) {
        // always put preprocessor stuff upfront
        return 0;
    }
    return -2;
}

/**
 * Process a newline character.
 * This function is called whenever the user hits <return/enter>.
 */
function indent(line, indentWidth, ch)
{
    gIndentWidth = indentWidth;
    gMode = document.highlightingModeAt(new Cursor(line, document.lineLength(line)));
    var alignOnly = (ch == "");

    if (ch != '\n' && !alignOnly)
        return processChar(line, ch);

    return indentLine(line, alignOnly);
}

// kate: space-indent on; indent-width 4; replace-tabs on;
