var katescript = { "name": "Ruby", "author": "Robin Pedersen ", "license": "LGPL", "revision": 2, "kate-version": "5.1", "required-syntax-style": "ruby", "indent-languages": ["ruby"] }; // 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 ("cursor.js"); require ("range.js"); //BEGIN USER CONFIGURATION //END USER CONFIGURATION // specifies the characters which should trigger indent, beside the default '\n' var triggerCharacters = "cdefhilnrsuw}]"; // Indent after lines that match this regexp var rxIndent = /^\s*(((public|protected|private)\s+)?def|if|unless|for|while|until|class|module|else|elsif|case|when|begin|rescue|ensure|catch)\b/; // Unindent lines that match this regexp var rxUnindent = /^\s*((end|when|else|elsif|rescue|ensure)\b|[\]\}])(.*)$/; var debugMode = false; function dbg() { if (debugMode) { debug.apply(this, arguments); } } function assert(cond) { if (!cond) throw "assertion failure"; } // Return the closest non-empty line, ignoring comments // (result <= line). Return -1 if the document function findPrevNonCommentLine(line) { line = document.prevNonEmptyLine(line); while (line >= 0 && document.isComment(line, document.firstColumn(line))) { line = document.prevNonEmptyLine(line - 1); } return line; } function isLineContinuing(line) { return /\\$/.test(document.line(line)); } // Return true if the given column is at least equal to the column that // contains the last non-whitespace character at the given line, or if // the rest of the line is a comment. function isLastCodeColumn(line, column) { if (column >= document.lastColumn(line)) return true; else if (document.isComment(line, document.nextNonSpaceColumn(line, column+1))) return true; else return false; } // Look for a pattern at the end of the statement. // // Returns true if the pattern is found, in a position // that is not inside a string or a comment, and the position + // the length of the matching part is either the end of the // statement, or a comment. // // The regexp must be global, and the search is continued until // a match is found, or the end of the string is reached. function testAtEnd(stmt, rx) { assert(rx.global); var cnt = stmt.content(); var res; // Work-around QML bug: rx.lastIndex = 0; while ((res = rx.exec(cnt)) !== null) { var start = res.index; var end = rx.lastIndex; if (stmt.isCode(start)) { if (end == cnt.length) return true; if (stmt.isComment(end)) return true; } } return false; } function isStmtContinuing(line) { // Is there an open parenthesis? var anch = lastAnchor(line+1, 0); if (anch.line >= 0) return true; var stmt = new Statement(line, line); var rx = /((\+|\-|\*|\/|\=|&&|\|\||\band\b|\bor\b|,)\s*)/g; return testAtEnd(stmt, rx); } // Return the first line that is not preceded by a "continuing" line. // Return currLine if currLine <= 0 function findStmtStart(currLine) { var p, l = currLine; do { if (l <= 0) return l; p = l; l = findPrevNonCommentLine(l - 1); } while ((l == p-1 && isLineContinuing(l)) || isStmtContinuing(l)); return p; } // Statement class function Statement(start, end) { this.start = start; this.end = end; // Convert to string for debugging this.toString = function() { return "{" + this.start + "," + this.end + "}"; } // Return an object having 'line' and 'column' set to the given offset in a statement this.offsetToCursor = function(offset) { // TODO Provide helper function for this when API is converted to using cursors: var line = this.start; while (line < this.end && document.lineLength(line) < offset) { offset -= document.lineLength(line++) + 1; } return new Cursor(line, offset); } // Return document.attribute at the given offset in a statement this.attribute = function(offset) { var cur = this.offsetToCursor(offset); return document.attribute(cur.line, cur.column); } // Return document.isCode at the given offset in a statement this.isCode = function(offset) { var cur = this.offsetToCursor(offset); return document.isCode(cur.line, cur.column); } // Return document.isComment at the given offset in a statement this.isComment = function(offset) { var cur = this.offsetToCursor(offset); return document.isComment(cur.line, cur.column); } // Return the indent at the beginning of the statement this.indent = function() { return document.firstVirtualColumn(this.start); } // Return the content of the statement from the document this.content = function() { var cnt = ""; for (var l = this.start; l <= this.end; l++) { cnt += document.line(l).replace(/\\$/, " "); if (l < this.end) cnt += " "; } return cnt; } } // Returns a tuple that contains the first and last line of the // previous statement before line. function findPrevStmt(line) { var stmtEnd = findPrevNonCommentLine(line); var stmtStart = findStmtStart(stmtEnd); return new Statement(stmtStart, stmtEnd); } function isBlockStart(stmt) { var cnt = stmt.content(); var len = cnt.length; if (rxIndent.test(cnt) && !isRuby3EndlessMethodDefinition(stmt)) return true; var rx = /((\bdo\b|\{)(\s*\|.*\|)?\s*)/g; return testAtEnd(stmt, rx); } function isRuby3EndlessMethodDefinition(stmt) { var rx = /^\s*def\s+([^\s(=]+)(\s*\([^)]*\))?\s+=/; return rx.test(stmt.content()); } function isBlockEnd(stmt) { var cnt = stmt.content(); return rxUnindent.test(cnt); } function findBlockStart(line) { var nested = 0; var stmt = new Statement(line, line); while (true) { if (stmt.start < 0) return stmt; stmt = findPrevStmt(stmt.start - 1); if (isBlockEnd(stmt)) { nested++; } if (isBlockStart(stmt)) { if (nested == 0) return stmt; else nested--; } } } // check if the trigger characters are in the right context, // otherwise running the indenter might be annoying to the user function isValidTrigger(line, ch) { if (ch == "" || ch == "\n") return true; // Explicit align or new line var res = rxUnindent.exec(document.line(line)); if (res) { if (res[3] == "") return true; // Exact match } return false; } // Helper function to compare two KTextEditor::Cursor objects. // Returns 0 if equal, 1 if ca > cb, -1 if ca < cb function compare(ca, cb) { if (ca.line != cb.line) { return (ca.line > cb.line) ? 1 : -1; } else if (ca.column != cb.column) { return (ca.column > cb.column) ? 1 : -1; } else { return 0; } } // Find the last open bracket before the current line. // Result is a KTextEditor::Cursor object, with an extra attribute, 'ch' // containing the type of bracket. function lastAnchor(line, column) { var anch = document.anchor(line, column, '('); anch.ch = '('; var tmp1 = document.anchor(line, column, '{'); tmp1.ch = '{'; var tmp2 = document.anchor(line, column, '['); tmp2.ch = '['; if (compare(tmp1, anch) == 1) anch = tmp1; if (compare(tmp2, anch) == 1) anch = tmp2; return anch; } // indent gets three arguments: line, indentWidth in spaces, // typed character indent function indent(line, indentWidth, ch) { if (!isValidTrigger(line, ch)) return -2; var prevStmt = findPrevStmt(line - 1); if (prevStmt.end < 0) return -2; // Can't indent the first line var prev = document.prevNonEmptyLine(line); // HACK Detect here documents if (document.isAttributeName(prev, document.lineLength(prev)-1, "Ruby:Here Document")) { return -1; // HERE-DOCUMENT } // HACK Detect embedded comments if (document.isAttributeName(prev, document.lineLength(prev)-1, "Ruby:Blockcomment")) { return -1; } var prevStmtCnt = prevStmt.content(); var prevStmtInd = prevStmt.indent(); // Are we inside a parameter list, array or hash? var anch = lastAnchor(line, 0); if (anch.line >= 0) { var shouldIndent = (anch.line == prevStmt.end) || testAtEnd(prevStmt, /,\s*/g); if (!isLastCodeColumn(anch.line, anch.column) || lastAnchor(anch.line, anch.column).line >= 0) { // TODO This is alignment, should force using spaces instead of tabs: if (shouldIndent) { anch.column += 1; var nextCol = document.nextNonSpaceColumn(anch.line, anch.column); if (nextCol > 0 && !document.isComment(anch.line, nextCol)) anch.column = nextCol; } // Keep indent of previous statement, while aligning to the anchor column return [prevStmtInd, document.toVirtualColumn(anch.line, anch.column)]; } else { return document.firstVirtualColumn(anch.line) + (shouldIndent ? indentWidth : 0); } } // Handle indenting of multiline statements. if ((prevStmt.end == line-1 && isLineContinuing(prevStmt.end)) || isStmtContinuing(prevStmt.end)) { if (prevStmt.start == prevStmt.end) { if (ch == '' && document.firstVirtualColumn(line) > document.firstVirtualColumn(prevStmt.end)) { return -2; // Don't force a specific indent level when aligning manually } return prevStmtInd + indentWidth * 2; } else { return document.firstVirtualColumn(prevStmt.end); } } if (rxUnindent.test(document.line(line))) { var startStmt = findBlockStart(line); if (startStmt.start >= 0) return startStmt.indent(); else return -2; } if (isBlockStart(prevStmt)) { return prevStmtInd + indentWidth; } else if (prevStmtCnt.search(/[\[\{]\s*$/) != -1) { return prevStmtInd + indentWidth; } // Keep current return prevStmtInd; } // kate: indent-width 2; indent-spaces on;