File indexing completed on 2024-05-19 15:42:36

0001 /*
0002     SPDX-FileCopyrightText: 2012 Sven Brauch svenbrauch @googlemail.com
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "codehelpers.h"
0008 #include <QStack>
0009 
0010 namespace Python {
0011     
0012 
0013 FileIndentInformation::FileIndentInformation(const QStringList& lines)
0014 {
0015     initialize(lines);
0016 }
0017 
0018 void FileIndentInformation::initialize(const QStringList& lines)
0019 {
0020     m_indents.clear();
0021     for ( int atLine = 0; atLine < lines.length(); atLine++ ) {
0022         const QString& currentLine = lines.at(atLine);
0023         const int currentLength = currentLine.length();
0024         bool lineIsEmpty = true;
0025         for ( int indent = 0; indent < currentLength; indent++ ) {
0026             if ( ! currentLine.at(indent).isSpace() ) {
0027                 m_indents.append(indent);
0028                 lineIsEmpty = false;
0029                 break;
0030             }
0031         }
0032         if ( lineIsEmpty ) {
0033             m_indents.append(currentLine.length());
0034         }
0035     }
0036 }
0037 
0038 FileIndentInformation::FileIndentInformation(const QString& data)
0039 {
0040     initialize(data.split('\n'));
0041 }
0042 
0043 FileIndentInformation::FileIndentInformation(const QByteArray& data)
0044 {
0045     initialize(QString(data.data()).split('\n'));
0046 }
0047 
0048 FileIndentInformation::FileIndentInformation(KTextEditor::Document* document)
0049 {
0050     QStringList lines;
0051     for ( int i = 0; i < document->lines(); i++ ) {
0052         lines << document->line(i);
0053     }
0054     initialize(lines);
0055 }
0056 
0057 int FileIndentInformation::indentForLine(int line) const
0058 {
0059     return m_indents.at(line);
0060 }
0061 
0062 int FileIndentInformation::linesCount() const
0063 {
0064     return m_indents.length();
0065 }
0066 
0067 int FileIndentInformation::nextChange(int line, ChangeTypes type, ScanDirection direction) const
0068 {
0069     line = qMin(line, m_indents.length() - 1);
0070     line = qMax(line, 0);
0071     const int currentIndent = m_indents.at(line);
0072     const int length = m_indents.length();
0073     const char scandir = direction == Forward ? 1 : -1;
0074     int atIndent = 0;
0075     do {
0076         if ( line >= length - 1 || line < 0 ) {
0077            break;
0078         }
0079         line += scandir;
0080         atIndent = m_indents.at(line);
0081     } while ( type == Indent ? atIndent <= currentIndent :
0082               type == Dedent ? atIndent >= currentIndent :
0083                                atIndent == currentIndent );
0084     return line;
0085 }
0086 
0087 CodeHelpers::CodeHelpers()
0088 {
0089     
0090 }
0091 
0092 CodeHelpers::EndLocation CodeHelpers::endsInside(const QString &code)
0093 {
0094     bool insideSingleLineComment = false;
0095     QStringList stringDelimiters;
0096     stringDelimiters << "\"\"\"" << "\'\'\'" << "'" << "\"";
0097     QStack<QString> stringStack;
0098     const int max_len = code.length();
0099     for ( int atChar = 0; atChar < max_len; atChar++ ) {
0100         const QChar c = code.at(atChar);
0101         if ( c == ' ' || c.isLetterOrNumber() ) {
0102             continue;
0103         }
0104         if ( stringStack.isEmpty() && c == '#' ) {
0105             insideSingleLineComment = true;
0106             continue;
0107         }
0108         if ( c == '\n' ) {
0109             insideSingleLineComment = false;
0110             continue;
0111         }
0112         if ( insideSingleLineComment ) {
0113             // don't count string delimiters in a comment line
0114             continue;
0115         }
0116         if ( c != '"' && c != '\'' && c != '\\' ) {
0117             continue;
0118         }
0119         QStringRef t;
0120         if ( max_len - atChar > 2 ) {
0121             t = code.midRef(atChar, 3);
0122         }
0123         foreach ( const QString& check, stringDelimiters ) {
0124             if ( t != check && ! ( check.size() == 1 && c == check.at(0) ) ) {
0125                 continue;
0126             }
0127             if ( stringStack.isEmpty() ) {
0128                 stringStack.push(check);
0129                 atChar += check.length() - 1;
0130                 break;
0131             }
0132             else if ( stringStack.top() == check ) {
0133                 stringStack.pop();
0134                 atChar += check.length() - 1;
0135                 break;
0136             }
0137         }
0138         if ( c == '\\' ) {
0139             atChar ++;
0140             continue;
0141         }
0142     }
0143 
0144     if ( ! stringStack.isEmpty() ) {
0145         return String;
0146     }
0147     else if ( insideSingleLineComment ) {
0148         return Comment;
0149     }
0150 
0151     return Code;
0152 }
0153 
0154 QString CodeHelpers::killStrings(QString stringWithStrings)
0155 {
0156     QRegExp replaceStrings("(\".*\"|\'.*\'|\"\"\".*\"\"\"|\'\'\'.*\'\'\')");
0157     replaceStrings.setMinimal(true);
0158     QString stripped = stringWithStrings.replace(replaceStrings, "\"S\"");
0159     return stripped;
0160 }
0161 
0162 QString CodeHelpers::expressionUnderCursor(Python::LazyLineFetcher& lineFetcher, KTextEditor::Cursor cursor,
0163                                            KTextEditor::Cursor& startCursor, bool forceScanExpression)
0164 {
0165     startCursor = cursor;
0166     QString line = lineFetcher.fetchLine(cursor.line());
0167     int index = cursor.column();
0168     QChar c = line[index];
0169     
0170     int end = index;
0171     // This flag is used by codecompletion (in contrast to the debugger)
0172     if ( ! forceScanExpression ) {
0173         if ( ! c.isLetterOrNumber() && c != '_' ) {
0174             return QString();
0175         }
0176     }
0177     for (; end < line.size(); ++end)
0178     {
0179         QChar c = line[end];
0180         if ( ! ( c.isLetterOrNumber() || c == '_' ) ) {
0181             if ( ! forceScanExpression ) end--;
0182             break;
0183         }
0184     }
0185     int start = index;
0186     QStringList openingBrackets = QStringList() << "(" << "[" << "{" << "\"" << "'";
0187     QStringList closingBrackets = QStringList() << ")" << "]" << "}" << "\"" << "'";
0188     QStringList sliceChars = QStringList() << "." << "(" << "["; // chars which are allowed to be preceded by a space
0189     QStringList seperatorChars = QStringList() << "," << "=" << ":" << "*" << "-" << "+" << "/" << "%" << "^" << "~";
0190     QStack<QString> brackets;
0191     bool lastWasSlice = false;
0192     int linesFetched = 1;
0193     QString text;
0194     bool done = false;
0195     while ( start != 0 ) {
0196         while ( start >= 0 ) {
0197             QChar c = line[start];
0198             int bracket = closingBrackets.indexOf(c);
0199             if ( ! brackets.isEmpty() && brackets.top() == c ) {
0200                 brackets.pop();
0201             }
0202             else if ( bracket != -1 ) {
0203                 brackets.push(openingBrackets.at(bracket));
0204             }
0205             else if ( openingBrackets.contains(c) ) {
0206                 start += 1;
0207                 done = true;
0208                 break;
0209             }
0210             
0211             if ( brackets.isEmpty() && ( (c.isSpace() && ! lastWasSlice) || seperatorChars.contains(c) ) ) {
0212                 done = true;
0213                 start += 1;
0214                 break;
0215             }
0216             
0217             if ( sliceChars.contains(c) ) {
0218                 lastWasSlice = true;
0219             }
0220             else {
0221                 lastWasSlice = false;
0222             }
0223             start--;
0224         }
0225         if ( done ) {
0226             break;
0227         }
0228         if ( cursor.line() < linesFetched ) {
0229             break;
0230         }
0231         if ( brackets.isEmpty() && ! lineFetcher.fetchLine(cursor.line() - linesFetched).trimmed().endsWith('\\') ) {
0232             // break at newline without previous backslash
0233             break;
0234         }
0235         // store this line for multi-line expressions
0236         if ( linesFetched != 1 ) {
0237             text.prepend(line);
0238         }
0239         else {
0240             text.prepend(line.mid(0, end));
0241         }
0242         line.clear();
0243         while ( line.isEmpty() && cursor.line() >= linesFetched ) {
0244             line = lineFetcher.fetchLine(cursor.line() - linesFetched);
0245             linesFetched++;
0246             start = line.length() - 1;
0247         }
0248     }
0249     start = qMax(0, start);
0250     QString linePart;
0251     if ( start > end ) {
0252         linePart = QString();
0253     }
0254     else {
0255         linePart = line.mid(start, end-start + 1);
0256         startCursor.setColumn(start);
0257     }
0258 
0259     QString expression(linePart + text);
0260     expression = expression.trimmed();
0261     return expression;
0262 }
0263 
0264 QString CodeHelpers::extractStringUnderCursor(const QString &code, KTextEditor::Range range, KTextEditor::Cursor cursor, int *cursorPositionInString = nullptr)
0265 {
0266     QPair<QString, QString> beforeAndAfter = splitCodeByCursor(code, range, cursor);
0267 
0268     // TODO is this even necessary?
0269     if ( endsInside(beforeAndAfter.first) != String ) {
0270         return QString();
0271     }
0272 
0273     QStringList quoteCharacters = QStringList() << "\"" << "'";
0274 
0275     // using a stack is probably overkill for this...
0276     QStack<QString> quotes;
0277     QString string;
0278 
0279     int start = beforeAndAfter.first.size() - 1;
0280 
0281 
0282     while ( start >= 0 ) {
0283         QChar c = beforeAndAfter.first.at(start);
0284         int quote = quoteCharacters.indexOf(c);
0285 
0286         // 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
0287         if ( quote != -1 && (start == 0 || (start != 0 && beforeAndAfter.first.at(start - 1) != '\\')) ) {
0288             if ( endsInside(beforeAndAfter.first.left(start)) != String ) {
0289                 quotes.push(quoteCharacters.at(quote));
0290                 break;
0291             }
0292         }
0293 
0294         start--;
0295     }
0296 
0297     int end = start + 1;
0298 
0299     while ( ! quotes.isEmpty() && end < beforeAndAfter.first.size() + beforeAndAfter.second.size() ) {
0300         QChar c;
0301         if (end < beforeAndAfter.first.size()) {
0302             c = beforeAndAfter.first.at(end);
0303         }
0304         else {
0305             c = beforeAndAfter.second.at(end - beforeAndAfter.first.size());
0306         }
0307 
0308         if (c == '\\') {
0309             end += 2;
0310         }
0311 
0312         if ( quotes.top() == c ) {
0313             quotes.pop();
0314         }
0315 
0316         end++;
0317     }
0318 
0319     string = code.mid(start, end - start);
0320 
0321     if ( cursorPositionInString ) {
0322         *cursorPositionInString = beforeAndAfter.first.size() - start;
0323     }
0324 
0325     return string;
0326 }
0327 
0328 QPair<QString, QString> CodeHelpers::splitCodeByCursor(const QString &code, KTextEditor::Range range, KTextEditor::Cursor cursor)
0329 {
0330     QStringList lines = code.split('\n');
0331 
0332     int position = 0;
0333     bool firstRow = true;
0334     int startColumn = range.start().column();
0335     int endColumn;
0336     for ( int row = range.start().line(), i = 0; row <= cursor.line(); row++, i++ ) {
0337         if ( row != cursor.line() ) {
0338             if ( i >= lines.size() ) {
0339                 // something went wrong with the context ranges
0340                 break;
0341             }
0342             endColumn = lines.at(i).size();
0343         }
0344         else {
0345             endColumn = cursor.column();
0346         }
0347 
0348         position += endColumn - startColumn + 1;
0349 
0350         if ( firstRow ) {
0351             startColumn = 0;
0352             firstRow = false;
0353         }
0354     }
0355 
0356     QString before(code.mid(0, position - 1));
0357     QString after(code.mid(position - 1, code.size() - position + 1));
0358 
0359     return QPair<QString, QString>(before, after);
0360 }
0361 
0362 }