diff --git a/src/script/data/indentation/r.js b/src/script/data/indentation/r.js new file mode 100644 --- /dev/null +++ b/src/script/data/indentation/r.js @@ -0,0 +1,413 @@ +var katescript = { + "name": "R", + "author": "Pierre de Villemerereuil ", + "license": "LGPL", + "revision": 1, + "kate-version": "5.1", + "indent-languages": ["R", "R Script", "Script R"] +}; // kate-script-header, must be at the start of the file without comments + +// Some functions modified from Python indentation file +// (credit to Paul Giannaros , Gerald Senarclens de Grancy ) + +// required katepart js libraries +require ("range.js"); +require ("string.js"); +require ("utils.js") + +triggerCharacters = '})]'; + +openings = ['(', '[','{']; +closings = [')', ']','}']; // requires same order as in openings +operators = ['+', '-', '*', '/', '^', + '&', '|', '==', '>', '<', '<=', '>=', '!=', + '%%', '%*%', '%/%', '%in%', + '%>%', '%T>%', '%$%', '%<>%']; +equalOperatorSigns = ['=', '>', '<', '!']; + +// Extension of endswith for an array of tests +function endsWithAny(suffixes, string) { + return suffixes.some(function (suffix) { + return string.endsWith(suffix); + }); +} + +// Return the given line without comments and leading or trailing whitespace. +// but keep strings (compared to getCode) +// Eg. +// getCode(x) -> "for i in range(3):" +// if document.line(x) == " for i in range(3):" +// getCode(x) -> "for i in range(3):" +// if document.line(x) == "for i in range(3): " +// getCode(x) -> "for i in range(3):" +// if document.line(x) == "for i in range(3): # grand" +function getCodeWithString(lineNr) { + var line = document.line(lineNr); + var code = ''; + for (var position = 0; position < line.length; position++) { + if (document.isCode(lineNr, position) || document.isString(lineNr, position) || document.isOthers(lineNr, position)) { + code += line[position]; + } + } + return code.trim(); +} + +// Return the given line without comments and leading or trailing whitespace. +// Eg. +// getCode(x) -> "for i in range(3):" +// if document.line(x) == " for i in range(3):" +// getCode(x) -> "for i in range(3):" +// if document.line(x) == "for i in range(3): " +// getCode(x) -> "for i in range(3):" +// if document.line(x) == "for i in range(3): # grand" +function getCode(lineNr) { + var line = document.line(lineNr); + var code = ''; + for (var position = 0; position < line.length; position++) { + if (document.isCode(lineNr, position)) { + code += line[position]; + } + } + return code.trim(); +} + +// Returns the number of spaces after "pos"" +// on the line number "lineNr" +function countSpaces(lineNr, pos) { + var line = document.line(lineNr); + var add = 0; + var pos = pos + 1; + while (line[pos] == " ") { + add++; + pos++; + } + return add; +} + +// Returns the indent if finding a mismatch using brackets, commas and equal signs +// If there are no mismatch, -1 is returned. +// `lineNr`: number of the line for which the indent is calculated +function calcMismatchIndent(lineNr, add) { + // initialising some counters + var countClosing = new Array(); + closings.forEach(function(elem) { + countClosing[elem] = 0; + }); + if (closings.indexOf(add) > -1) { + countClosing[add]++; + } + var countComma = 0; + + // starting looking for mismatches + for (var i = lineNr; i >= 0; --i) { + var lastOpen = document.line(i).length; + var lineString = document.line(i); + for (var j = lineString.length; j >= 0; --j) { + // Ignore comments and strings + if (document.isComment(i, j) || document.isString(i, j)) + continue; + + // Testing for brackets + // If a closing bracket, add 1 to counter + if (closings.indexOf(lineString[j]) > -1) { + countClosing[lineString[j]]++; + } + // If an opening bracket, add 1 to the corresponding closing counter + var index = openings.indexOf(lineString[j]); + if (index > -1) { + countClosing[closings[index]]--; + // If an open-but-not-closed bracket is found + // Return indent corresponding to its position + if (countClosing[closings[index]] == -1) + return {indent : j + 1, line : i, pos: lastOpen, type : "unclosed"}; + // Otherwise just update "lastOpen" to j + lastOpen = j; + } + // If the start of the line is reached and + // no comma was "opened" + if (j == document.firstVirtualColumn(i) && countComma == 0) { + // Test if all brackets are closed + var allclosed = true; + for (var key in countClosing) { + if (countClosing[key] != 0) { + allclosed = false; + } + } // If they are all closed, return the indent of this line + if (allclosed) { + // if we didn't move, return -1 (keep indent), else return the indent of line i + if (i == lineNr) { + return {indent : -1, line : i, pos: lastOpen, type : "allclosed"}; + } else { + return {indent : j, line : i, pos: lastOpen, type : "allclosed"}; + } + } + + } + + // Counting the commas if needed + if (lineString[j] == ',') { + // If not between comma-inducing brackets + if (countClosing[')'] == 0 && countClosing[']'] == 0) + countComma++; + } + +// // Handling equal signs +// if (lineString[j] == "=" && equalOperatorSigns.indexOf(lineString[j - 1]) == -1 && lineString[j + 1] != "=") { +// // If not between equal-inducing brackets +// if (countClosing[')'] == 0 && countClosing[']'] == 0) { +// // If no comma is "closing" the equal +// if (countComma == 0) { +// // Return the position of equal to create a new indent +// return {indent : j + 1 + countSpaces(i,j), line : i, type : "equal"}; +// } +// } +// } + } + } + return {indent : -1, line : i, type : "unknown"}; +} + +// Find the position of an align operator with in the focus line +// accounting for opening and closing brackets +// `lineNr`: number of the line to search the align operator in +// Returns -1 if nothing was found +function findAlignOperator(lineNr, pos) { + var lineString = document.line(lineNr); + // initialising some counters + var countClosing = new Array(); + closings.forEach(function(elem) { + countClosing[elem] = 0; + }); + + for (var j = pos - 1; j >= 0; --j) { + // Ignore comments and strings + if (document.isComment(lineNr, j) || document.isString(lineNr, j)) + continue; + + // Testing for brackets + // If a closing bracket, add 1 to counter + if (closings.indexOf(lineString[j]) > -1) { + countClosing[lineString[j]]++; + } + + // If an opening bracket, add 1 to the corresponding closing counter + var index = openings.indexOf(lineString[j]); + if (index > -1) { + countClosing[closings[index]]--; + // If an open-but-not-closed bracket is found + // Return -1 because nothing was found + if (countClosing[closings[index]] == -1) + return -1 + } + + + if (lineString.substr(j - 1, 2) == "<-" || + (lineString[j] == "=" && equalOperatorSigns.indexOf(lineString[j - 1]) == -1 && lineString[j + 1] != "=") || + lineString[j] == "~") + { + // Test if all brackets are closed + var allclosed = true; + for (var key in countClosing) { + if (countClosing[key] != 0) { + allclosed = false; + } + } // If they are all closed, return the indent of this line + if (allclosed) { + return j; + } + } + } + + return -1 +} + +// Returns the indent based on operators +// `lineNr`: number of the line for which the indent is calculated +// `indentWidth` : indent width +// `lineLastOp` : does the line on which returns was hit end with an operator +// (note that the line for lineLastOp is not necessarily lineNr) +function calcOperatorIndent(lineNr, indentWidth, pos, lineLastOp) { + var currentIndent = document.firstVirtualColumn(lineNr); + var refLine = lineNr; + // If we haven't indented yet and line ends up with an operator + // then indent +// if (currentIndent == 0 && lineLastOp) { +// return indentWidth; +// } + // If the current line ends up with an operator + if (lineLastOp) { + var previousLine = getCodeWithString(refLine - 1); + while (previousLine == '' && refLine >= 0) { + refLine = refLine - 1; + previousLine = getCodeWithString(refLine - 1); + } + // If the line before the indent line doesn't ends up with an operator + if (!endsWithAny(operators, previousLine)) { + // then look for align operator + locAlign = findAlignOperator(lineNr, pos); + if (locAlign != -1) { + return locAlign + 1 + countSpaces(lineNr, locAlign); + } else { + // if no align operator, simply indent + return currentIndent + indentWidth; + } + } else { + // else don't + return currentIndent; + } + } else { + var previousLine = getCodeWithString(refLine - 1); + while (previousLine == '' && refLine >= 0) { + refLine = refLine - 1; + previousLine = getCodeWithString(refLine - 1); + } + + // If the previous line ends with an operator + if (endsWithAny(operators, previousLine)) { + // Looking for the start of the operator indenting + for (i = refLine - 1; i>=0; --i) { + // If we indented in the past + if (document.firstVirtualColumn(i) < currentIndent) { + currentIndent = document.firstVirtualColumn(i); + var previousLine = getCodeWithString(i - 1); + // and doesn't end up with an operator + if (!endsWithAny(operators, previousLine)) { + //return this line indent otherwise + return currentIndent; + } + } + } + } else { + return currentIndent; + } + + // If the current line doesn't ends up with an operator, we might need to unindent + // Let's look above + for (i = refLine; i>=0; --i) { + // If a line has a lower indent + if (document.firstVirtualColumn(i) <= currentIndent) { + currentIndent = document.firstVirtualColumn(i); + var previousLine = getCodeWithString(i - 1); + // and doesn't end up with an operator + if (!endsWithAny(operators, previousLine)) { + //return this line indent + return currentIndent; + } + } + } + } + return -1; +} + +// Align when a closing bracket was entered +// `lineNr`: number of the line for which the indent is calculated +// `c` : the bracket that was entered +function alignBrackets(lineNr, lastChar, newChar, indentWidth) { + var charsMatch = ( lastChar == '(' && newChar == ')' ) || + ( lastChar == '{' && newChar == '}' ) || + ( lastChar == '[' && newChar == ']' ); + if (charsMatch) { + indentation = document.firstVirtualColumn(lineNr - 1); + document.insertText(lineNr, document.firstColumn(lineNr), "\n"); + view.setCursorPosition(lineNr, document.line(lineNr).length); + document.indent(new Range(lineNr + 1, 0, lineNr + 1, 1), indentation / indentWidth); + } + return document.firstVirtualColumn(lineNr - 1) + indentWidth; +} + + +// Return the amount of characters (in spaces) to be indented. +// Special indent() return values: +// -2 = no indent +// -1 = keep last indent +function indent(line, indentWidth, ch) { + if (line == 0) // don't ever act on document's first line + return -2; + var lastLine = getCodeWithString(line - 1); + var lastChar = lastLine.substr(-1); + var newChar = getCodeWithString(line).substr(0, 1); + + // Align if a closing bracket was entered + // (note that the code above might also be triggered if a bracket is closed right after an opened one) + if (closings.indexOf(ch) > -1) { + //if the entered bracket doesn't start the line, do nothing + if (newChar != ch) { + return document.firstVirtualColumn(line); + } + var matchOpen = openings[closings.indexOf(ch)]; + if (lastChar == matchOpen) { + return alignBrackets(line, lastChar, newChar, indentWidth) + } else { + mismatch = calcMismatchIndent(line - 1, ch); + return document.firstVirtualColumn(mismatch.line); + } + } + + // if empty line, strictly keep indent + // (-1 seems to be not strict and "restore" latest indent with text) + if (!lastLine.length) { + return countSpaces(line - 1, -1); + } + + // opening brackets and returns: indent (and unindent following bracket if needed) + if (openings.indexOf(lastChar) > -1) { + return alignBrackets(line, lastChar, newChar, indentWidth); + } + + // calculate indents based on mismatch of brackets, commas and equal signs + var mismatch = calcMismatchIndent(line - 1, ''); + var indent = mismatch.indent; + +// debug("mismatch.line = " + mismatch.line) +// debug("mismatch.pos = " + mismatch.pos) +// debug("mismatch.indent = " + mismatch.indent) +// debug("mismatch.type = " + mismatch.type) + + // if indent is based on non-opened brackets, try indent because of operators + // Don't do it if the end is "<-" though (necessary because "-" is an operator...) + if (mismatch.type == "allclosed" && !lastLine.endsWith('<-')) { + // compute indent due to an operator + indent = calcOperatorIndent(mismatch.line, indentWidth, mismatch.pos, endsWithAny(operators, lastLine)); + } + + if (mismatch.type == "unclosed" && endsWithAny(operators, lastLine)) { + // look whether we should account for an align operator + locAlign = findAlignOperator(mismatch.line, mismatch.pos); + if (locAlign != -1) { + indent = locAlign + 1 + countSpaces(mismatch.line, locAlign); + } + } + + // if unclosed bracket is found, check that the line didn't end with this bracket + // if it did, do not change anything to the indent + if (mismatch.type == "unclosed" && mismatch.indent == document.line(mismatch.line).length) { + indent = document.firstVirtualColumn(mismatch.line + 1); + } + + // At that point, we might have computed an indent equal to the current one, + // let's keep it simple and set indent to -1 in that case + if (document.firstVirtualColumn(line - 1) == indent) { + indent = -1; + } + + // If the next character is a closing bracket, + // let's see if we should indent back to the corresponding indent + if (closings.indexOf(newChar) > -1) { + mismatch = calcMismatchIndent(line - 1, newChar); + // change indent only if the closing bracket matches a "final" one + if (endsWithAny(openings, getCodeWithString(mismatch.line))) { + return document.firstVirtualColumn(mismatch.line); + } + } + + // Assignment is important and particular, so always indent when we do it + if (getCodeWithString(line - 1).endsWith('<-') || getCodeWithString(line - 1).endsWith('=')) { + if (indent > -1) + indent += indentWidth; + else + indent = document.firstVirtualColumn(line - 1) + indentWidth; + } + return indent; +} + +// kate: space-indent on; indent-width 4; replace-tabs on;