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 }