/*
This file is part of kdev-python, the python language plugin for KDevelop
Copyright (C) 2012 Sven Brauch svenbrauch@googlemail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "codehelpers.h"
#include
namespace Python {
FileIndentInformation::FileIndentInformation(const QStringList& lines)
{
initialize(lines);
}
void FileIndentInformation::initialize(const QStringList& lines)
{
m_indents.clear();
for ( int atLine = 0; atLine < lines.length(); atLine++ ) {
const QString& currentLine = lines.at(atLine);
const int currentLength = currentLine.length();
bool lineIsEmpty = true;
for ( int indent = 0; indent < currentLength; indent++ ) {
if ( ! currentLine.at(indent).isSpace() ) {
m_indents.append(indent);
lineIsEmpty = false;
break;
}
}
if ( lineIsEmpty ) {
m_indents.append(currentLine.length());
}
}
}
FileIndentInformation::FileIndentInformation(const QString& data)
{
initialize(data.split('\n'));
}
FileIndentInformation::FileIndentInformation(const QByteArray& data)
{
initialize(QString(data.data()).split('\n'));
}
FileIndentInformation::FileIndentInformation(KTextEditor::Document* document)
{
QStringList lines;
for ( int i = 0; i < document->lines(); i++ ) {
lines << document->line(i);
}
initialize(lines);
}
int FileIndentInformation::indentForLine(int line) const
{
return m_indents.at(line);
}
int FileIndentInformation::linesCount() const
{
return m_indents.length();
}
int FileIndentInformation::nextChange(int line, ChangeTypes type, ScanDirection direction) const
{
line = qMin(line, m_indents.length() - 1);
line = qMax(line, 0);
const int currentIndent = m_indents.at(line);
const int length = m_indents.length();
const char scandir = direction == Forward ? 1 : -1;
int atIndent = 0;
do {
if ( line >= length - 1 || line < 0 ) {
break;
}
line += scandir;
atIndent = m_indents.at(line);
} while ( type == Indent ? atIndent <= currentIndent :
type == Dedent ? atIndent >= currentIndent :
atIndent == currentIndent );
return line;
}
CodeHelpers::CodeHelpers()
{
}
CodeHelpers::EndLocation CodeHelpers::endsInside(const QString &code)
{
bool insideSingleLineComment = false;
QStringList stringDelimiters;
stringDelimiters << "\"\"\"" << "\'\'\'" << "'" << "\"";
QStack stringStack;
const int max_len = code.length();
qDebug() << "Checking for comment line:" << code;
for ( int atChar = 0; atChar < max_len; atChar++ ) {
const QChar c = code.at(atChar);
if ( c == ' ' || c.isLetterOrNumber() ) {
continue;
}
if ( stringStack.isEmpty() && c == '#' ) {
insideSingleLineComment = true;
continue;
}
if ( c == '\n' ) {
insideSingleLineComment = false;
continue;
}
if ( insideSingleLineComment ) {
// don't count string delimiters in a comment line
continue;
}
if ( c != '"' && c != '\'' && c != '\\' ) {
continue;
}
QStringRef t;
if ( max_len - atChar > 2 ) {
t = code.midRef(atChar, 3);
}
foreach ( const QString& check, stringDelimiters ) {
if ( t != check && ! ( check.size() == 1 && c == check.at(0) ) ) {
continue;
}
if ( stringStack.isEmpty() ) {
stringStack.push(check);
atChar += check.length() - 1;
break;
}
else if ( stringStack.top() == check ) {
stringStack.pop();
atChar += check.length() - 1;
break;
}
}
if ( c == '\\' ) {
atChar ++;
continue;
}
}
if ( ! stringStack.isEmpty() ) {
return String;
}
else if ( insideSingleLineComment ) {
return Comment;
}
return Code;
}
QString CodeHelpers::killStrings(QString stringWithStrings)
{
QRegExp replaceStrings("(\".*\"|\'.*\'|\"\"\".*\"\"\"|\'\'\'.*\'\'\')");
replaceStrings.setMinimal(true);
QString stripped = stringWithStrings.replace(replaceStrings, "\"S\"");
return stripped;
}
QString CodeHelpers::expressionUnderCursor(Python::LazyLineFetcher& lineFetcher, KTextEditor::Cursor cursor, bool forceScanExpression)
{
QString line = lineFetcher.fetchLine(cursor.line());
int index = cursor.column();
QChar c = line[index];
int end = index;
// This flag is used by codecompletion (in contrast to the debugger)
if ( ! forceScanExpression ) {
if ( ! c.isLetterOrNumber() && c != '_' ) {
return QString();
}
}
for (; end < line.size(); ++end)
{
QChar c = line[end];
if ( ! ( c.isLetterOrNumber() || c == '_' ) ) {
if ( ! forceScanExpression ) end--;
break;
}
}
int start = index;
QStringList openingBrackets = QStringList() << "(" << "[" << "{" << "\"" << "'";
QStringList closingBrackets = QStringList() << ")" << "]" << "}" << "\"" << "'";
QStringList sliceChars = QStringList() << "." << "(" << "["; // chars which are allowed to be preceded by a space
QStringList seperatorChars = QStringList() << "," << "=" << ":" << "*" << "-" << "+" << "/" << "%" << "^" << "~";
QStack brackets;
bool lastWasSlice = false;
int linesFetched = 1;
QString text;
bool done = false;
while ( start != 0 ) {
while ( start >= 0 ) {
QChar c = line[start];
int bracket = closingBrackets.indexOf(c);
qDebug() << bracket << c;
if ( ! brackets.isEmpty() && brackets.top() == c ) {
brackets.pop();
}
else if ( bracket != -1 ) {
brackets.push(openingBrackets.at(bracket));
}
else if ( openingBrackets.contains(c) ) {
start += 1;
done = true;
break;
}
if ( brackets.isEmpty() && ( (c.isSpace() && ! lastWasSlice) || seperatorChars.contains(c) ) ) {
done = true;
start += 1;
break;
}
if ( sliceChars.contains(c) ) {
lastWasSlice = true;
}
else {
lastWasSlice = false;
}
start--;
}
if ( done ) {
break;
}
if ( cursor.line() < linesFetched ) {
break;
}
if ( brackets.isEmpty() && ! lineFetcher.fetchLine(cursor.line() - linesFetched).trimmed().endsWith('\\') ) {
// break at newline without previous backslash
break;
}
// store this line for multi-line expressions
if ( linesFetched != 1 ) {
text.prepend(line);
}
else {
text.prepend(line.mid(0, end));
}
line.clear();
while ( line.isEmpty() && cursor.line() >= linesFetched ) {
line = lineFetcher.fetchLine(cursor.line() - linesFetched);
linesFetched++;
start = line.length() - 1;
}
}
start = qMax(0, start);
QString linePart;
if ( start > end ) {
linePart = QString();
}
else {
qDebug() << line << start << end << end-start << line.length();
linePart = line.mid(start, end-start + 1);
}
QString expression(linePart + text);
expression = expression.trimmed();
qDebug() << "expression found:" << expression;
return expression;
}
QString CodeHelpers::extractStringUnderCursor(const QString &code, KTextEditor::Range range, KTextEditor::Cursor cursor, int *cursorPositionInString = 0)
{
QPair beforeAndAfter = splitCodeByCursor(code, range, cursor);
// TODO is this even necessary?
if ( endsInside(beforeAndAfter.first) != String ) {
return QString();
}
QStringList quoteCharacters = QStringList() << "\"" << "'";
// using a stack is probably overkill for this...
QStack quotes;
QString string;
int start = beforeAndAfter.first.size() - 1;
while ( start >= 0 ) {
QChar c = beforeAndAfter.first.at(start);
int quote = quoteCharacters.indexOf(c);
// qDebug() << quote << c;
// if we've found a quote character and we're either at the beginning of the code or the previous char is not a backslash
if ( quote != -1 && (start == 0 || (start != 0 && beforeAndAfter.first.at(start - 1) != '\\')) ) {
if ( endsInside(beforeAndAfter.first.left(start)) != String ) {
quotes.push(quoteCharacters.at(quote));
break;
}
}
start--;
}
int end = start + 1;
while ( ! quotes.isEmpty() && end < beforeAndAfter.first.size() + beforeAndAfter.second.size() ) {
QChar c;
if (end < beforeAndAfter.first.size()) {
c = beforeAndAfter.first.at(end);
}
else {
c = beforeAndAfter.second.at(end - beforeAndAfter.first.size());
}
if (c == '\\') {
end += 2;
}
if ( quotes.top() == c ) {
quotes.pop();
}
end++;
}
string = code.mid(start, end - start);
if ( cursorPositionInString ) {
*cursorPositionInString = beforeAndAfter.first.size() - start;
}
qDebug() << "string found:" << string;
return string;
}
QPair CodeHelpers::splitCodeByCursor(const QString &code, KTextEditor::Range range, KTextEditor::Cursor cursor)
{
QStringList lines = code.split('\n');
int position = 0;
bool firstRow = true;
int startColumn = range.start().column();
int endColumn;
for ( int row = range.start().line(), i = 0; row <= cursor.line(); row++, i++ ) {
if ( row != cursor.line() ) {
if ( i >= lines.size() ) {
// something went wrong with the context ranges
break;
}
endColumn = lines.at(i).size();
}
else {
endColumn = cursor.column();
}
position += endColumn - startColumn + 1;
if ( firstRow ) {
startColumn = 0;
firstRow = false;
}
}
QString before(code.mid(0, position - 1));
QString after(code.mid(position - 1, code.size() - position + 1));
return QPair(before, after);
}
}