diff --git a/src/buffer/katetextbuffer.cpp b/src/buffer/katetextbuffer.cpp index 23cb820d..e0f2aa65 100644 --- a/src/buffer/katetextbuffer.cpp +++ b/src/buffer/katetextbuffer.cpp @@ -1,1069 +1,1069 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "kateglobal.h" #include "katetextbuffer.h" #include "katesecuretextbuffer_p.h" #include "katetextloader.h" // this is unfortunate, but needed for performance #include "katedocument.h" #include "kateview.h" #include "katepartdebug.h" #ifndef Q_OS_WIN #include #include // sadly there seems to be no possibility in Qt to determine detailed error // codes about e.g. file open errors, so we need to resort to evaluating // errno directly on platforms that support this #define CAN_USE_ERRNO #endif #include #include #include #include #include #if 0 #define BUFFER_DEBUG qCDebug(LOG_KTE) #else #define BUFFER_DEBUG if (0) qCDebug(LOG_KTE) #endif namespace Kate { TextBuffer::TextBuffer(KTextEditor::DocumentPrivate *parent, int blockSize, bool alwaysUseKAuth) : QObject(parent) , m_document(parent) , m_history(*this) , m_blockSize(blockSize) , m_lines(0) , m_lastUsedBlock(0) , m_revision(0) , m_editingTransactions(0) , m_editingLastRevision(0) , m_editingLastLines(0) , m_editingMinimalLineChanged(-1) , m_editingMaximalLineChanged(-1) , m_encodingProberType(KEncodingProber::Universal) , m_fallbackTextCodec(nullptr) , m_textCodec(nullptr) , m_generateByteOrderMark(false) , m_endOfLineMode(eolUnix) , m_newLineAtEof(false) , m_lineLengthLimit(4096) , m_alwaysUseKAuthForSave(alwaysUseKAuth) { // minimal block size must be > 0 Q_ASSERT(m_blockSize > 0); // create initial state clear(); } TextBuffer::~TextBuffer() { // remove document pointer, this will avoid any notifyAboutRangeChange to have a effect m_document = nullptr; // not allowed during editing Q_ASSERT(m_editingTransactions == 0); // kill all ranges, work on copy, they will remove themself from the hash QSet copyRanges = m_ranges; qDeleteAll(copyRanges); Q_ASSERT(m_ranges.empty()); // clean out all cursors and lines, only cursors belonging to range will survive foreach (TextBlock *block, m_blocks) { block->deleteBlockContent(); } // delete all blocks, now that all cursors are really deleted // else asserts in destructor of blocks will fail! qDeleteAll(m_blocks); m_blocks.clear(); // kill all invalid cursors, do this after block deletion, to uncover if they might be still linked in blocks QSet copyCursors = m_invalidCursors; qDeleteAll(copyCursors); Q_ASSERT(m_invalidCursors.empty()); } void TextBuffer::invalidateRanges() { // invalidate all ranges, work on copy, they might delete themself... QSet copyRanges = m_ranges; foreach (TextRange *range, copyRanges) { range->setRange(KTextEditor::Cursor::invalid(), KTextEditor::Cursor::invalid()); } } void TextBuffer::clear() { // not allowed during editing Q_ASSERT(m_editingTransactions == 0); invalidateRanges(); // new block for empty buffer TextBlock *newBlock = new TextBlock(this, 0); newBlock->appendLine(QString()); // clean out all cursors and lines, either move them to newBlock or invalidate them, if belonging to a range foreach (TextBlock *block, m_blocks) { block->clearBlockContent(newBlock); } // kill all buffer blocks qDeleteAll(m_blocks); m_blocks.clear(); // insert one block with one empty line m_blocks.append(newBlock); // reset lines and last used block m_lines = 1; m_lastUsedBlock = 0; // reset revision m_revision = 0; // reset bom detection m_generateByteOrderMark = false; // reset the filter device m_mimeTypeForFilterDev = QStringLiteral("text/plain"); // clear edit history m_history.clear(); // we got cleared emit cleared(); } TextLine TextBuffer::line(int line) const { // get block, this will assert on invalid line int blockIndex = blockForLine(line); // get line return m_blocks.at(blockIndex)->line(line); } QString TextBuffer::text() const { QString text; // combine all blocks foreach (TextBlock *block, m_blocks) { block->text(text); } // return generated string return text; } bool TextBuffer::startEditing() { // increment transaction counter ++m_editingTransactions; // if not first running transaction, do nothing if (m_editingTransactions > 1) { return false; } // reset information about edit... m_editingLastRevision = m_revision; m_editingLastLines = m_lines; m_editingMinimalLineChanged = -1; m_editingMaximalLineChanged = -1; // transaction has started emit editingStarted(); if (m_document) emit m_document->KTextEditor::Document::editingStarted(m_document); // first transaction started return true; } bool TextBuffer::finishEditing() { // only allowed if still transactions running Q_ASSERT(m_editingTransactions > 0); // decrement counter --m_editingTransactions; // if not last running transaction, do nothing if (m_editingTransactions > 0) { return false; } // assert that if buffer changed, the line ranges are set and valid! Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged != -1 && m_editingMaximalLineChanged != -1)); Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged <= m_editingMaximalLineChanged)); Q_ASSERT(!editingChangedBuffer() || (m_editingMinimalLineChanged >= 0 && m_editingMinimalLineChanged < m_lines)); Q_ASSERT(!editingChangedBuffer() || (m_editingMaximalLineChanged >= 0 && m_editingMaximalLineChanged < m_lines)); // transaction has finished emit editingFinished(); if (m_document) emit m_document->KTextEditor::Document::editingFinished(m_document); // last transaction finished return true; } void TextBuffer::wrapLine(const KTextEditor::Cursor &position) { // debug output for REAL low-level debugging BUFFER_DEBUG << "wrapLine" << position; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // get block, this will assert on invalid line int blockIndex = blockForLine(position.line()); /** * let the block handle the wrapLine * this can only lead to one more line in this block * no other blocks will change * this call will trigger fixStartLines */ ++m_lines; // first alter the line counter, as functions called will need the valid one m_blocks.at(blockIndex)->wrapLine(position, blockIndex); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = position.line(); } if (position.line() <= m_editingMaximalLineChanged) { ++m_editingMaximalLineChanged; } else { m_editingMaximalLineChanged = position.line() + 1; } // balance the changed block if needed balanceBlock(blockIndex); // emit signal about done change emit lineWrapped(position); if (m_document) emit m_document->KTextEditor::Document::lineWrapped(m_document, position); } void TextBuffer::unwrapLine(int line) { // debug output for REAL low-level debugging BUFFER_DEBUG << "unwrapLine" << line; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // line 0 can't be unwrapped Q_ASSERT(line > 0); // get block, this will assert on invalid line int blockIndex = blockForLine(line); // is this the first line in the block? bool firstLineInBlock = (line == m_blocks.at(blockIndex)->startLine()); /** * let the block handle the unwrapLine * this can either lead to one line less in this block or the previous one * the previous one could even end up with zero lines * this call will trigger fixStartLines */ m_blocks.at(blockIndex)->unwrapLine(line, (blockIndex > 0) ? m_blocks.at(blockIndex - 1) : nullptr, firstLineInBlock ? (blockIndex - 1) : blockIndex); --m_lines; // decrement index for later fixup, if we modified the block in front of the found one if (firstLineInBlock) { --blockIndex; } // remember changes ++m_revision; // update changed line interval if ((line - 1) < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = line - 1; } if (line <= m_editingMaximalLineChanged) { --m_editingMaximalLineChanged; } else { m_editingMaximalLineChanged = line - 1; } // balance the changed block if needed balanceBlock(blockIndex); // emit signal about done change emit lineUnwrapped(line); if (m_document) emit m_document->KTextEditor::Document::lineUnwrapped(m_document, line); } void TextBuffer::insertText(const KTextEditor::Cursor &position, const QString &text) { // debug output for REAL low-level debugging BUFFER_DEBUG << "insertText" << position << text; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // skip work, if no text to insert if (text.isEmpty()) { return; } // get block, this will assert on invalid line int blockIndex = blockForLine(position.line()); // let the block handle the insertText m_blocks.at(blockIndex)->insertText(position, text); // remember changes ++m_revision; // update changed line interval if (position.line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = position.line(); } if (position.line() > m_editingMaximalLineChanged) { m_editingMaximalLineChanged = position.line(); } // emit signal about done change emit textInserted(position, text); if (m_document) emit m_document->KTextEditor::Document::textInserted(m_document, position, text); } void TextBuffer::removeText(const KTextEditor::Range &range) { // debug output for REAL low-level debugging BUFFER_DEBUG << "removeText" << range; // only allowed if editing transaction running Q_ASSERT(m_editingTransactions > 0); // only ranges on one line are supported Q_ASSERT(range.start().line() == range.end().line()); // start column <= end column and >= 0 Q_ASSERT(range.start().column() <= range.end().column()); Q_ASSERT(range.start().column() >= 0); // skip work, if no text to remove if (range.isEmpty()) { return; } // get block, this will assert on invalid line int blockIndex = blockForLine(range.start().line()); // let the block handle the removeText, retrieve removed text QString text; m_blocks.at(blockIndex)->removeText(range, text); // remember changes ++m_revision; // update changed line interval if (range.start().line() < m_editingMinimalLineChanged || m_editingMinimalLineChanged == -1) { m_editingMinimalLineChanged = range.start().line(); } if (range.start().line() > m_editingMaximalLineChanged) { m_editingMaximalLineChanged = range.start().line(); } // emit signal about done change emit textRemoved(range, text); if (m_document) emit m_document->KTextEditor::Document::textRemoved(m_document, range, text); } int TextBuffer::blockForLine(int line) const { // only allow valid lines if ((line < 0) || (line >= lines())) { qFatal("out of range line requested in text buffer (%d out of [0, %d[)", line, lines()); } // we need blocks and last used block should not be negative Q_ASSERT(!m_blocks.isEmpty()); Q_ASSERT(m_lastUsedBlock >= 0); /** * shortcut: try last block first */ if (m_lastUsedBlock < m_blocks.size()) { /** * check if block matches * if yes, just return again this block */ TextBlock *block = m_blocks[m_lastUsedBlock]; const int start = block->startLine(); const int lines = block->lines(); if (start <= line && line < (start + lines)) { return m_lastUsedBlock; } } /** * search for right block * use binary search * if we leave this loop not by returning the found element we have an error */ int blockStart = 0; int blockEnd = m_blocks.size() - 1; while (blockEnd >= blockStart) { // get middle and ensure it is OK int middle = blockStart + ((blockEnd - blockStart) / 2); Q_ASSERT(middle >= 0); Q_ASSERT(middle < m_blocks.size()); // facts bout this block TextBlock *block = m_blocks[middle]; const int start = block->startLine(); const int lines = block->lines(); // right block found, remember it and return it if (start <= line && line < (start + lines)) { m_lastUsedBlock = middle; return middle; } // half our stuff ;) if (line < start) { blockEnd = middle - 1; } else { blockStart = middle + 1; } } // we should always find a block qFatal("line requested in text buffer (%d out of [0, %d[), no block found", line, lines()); return -1; } void TextBuffer::fixStartLines(int startBlock) { // only allow valid start block Q_ASSERT(startBlock >= 0); Q_ASSERT(startBlock < m_blocks.size()); // new start line for next block TextBlock *block = m_blocks.at(startBlock); int newStartLine = block->startLine() + block->lines(); // fixup block for (int index = startBlock + 1; index < m_blocks.size(); ++index) { // set new start line block = m_blocks.at(index); block->setStartLine(newStartLine); // calculate next start line newStartLine += block->lines(); } } void TextBuffer::balanceBlock(int index) { /** * two cases, too big or too small block */ TextBlock *blockToBalance = m_blocks.at(index); // first case, too big one, split it if (blockToBalance->lines() >= 2 * m_blockSize) { // half the block int halfSize = blockToBalance->lines() / 2; // create and insert new block behind current one, already set right start line TextBlock *newBlock = blockToBalance->splitBlock(halfSize); Q_ASSERT(newBlock); m_blocks.insert(m_blocks.begin() + index + 1, newBlock); // split is done return; } // second case: possibly too small block // if only one block, no chance to unite // same if this is first block, we always append to previous one if (index == 0) { return; } // block still large enough, do nothing if (2 * blockToBalance->lines() > m_blockSize) { return; } // unite small block with predecessor TextBlock *targetBlock = m_blocks.at(index - 1); // merge block blockToBalance->mergeBlock(targetBlock); // delete old block delete blockToBalance; m_blocks.erase(m_blocks.begin() + index); } void TextBuffer::debugPrint(const QString &title) const { // print header with title printf("%s (lines: %d bs: %d)\n", qPrintable(title), m_lines, m_blockSize); // print all blocks for (int i = 0; i < m_blocks.size(); ++i) { m_blocks.at(i)->debugPrint(i); } } bool TextBuffer::load(const QString &filename, bool &encodingErrors, bool &tooLongLinesWrapped, int &longestLineLoaded, bool enforceTextCodec) { // fallback codec must exist Q_ASSERT(m_fallbackTextCodec); // codec must be set! Q_ASSERT(m_textCodec); /** * first: clear buffer in any case! */ clear(); /** * construct the file loader for the given file, with correct prober type */ Kate::TextLoader file(filename, m_encodingProberType); /** * triple play, maximal three loading rounds * 0) use the given encoding, be done, if no encoding errors happen * 1) use BOM to decided if Unicode or if that fails, use encoding prober, if no encoding errors happen, be done * 2) use fallback encoding, be done, if no encoding errors happen * 3) use again given encoding, be done in any case */ for (int i = 0; i < (enforceTextCodec ? 1 : 4); ++i) { /** * kill all blocks beside first one */ for (int b = 1; b < m_blocks.size(); ++b) { TextBlock *block = m_blocks.at(b); block->clearLines(); delete block; } m_blocks.resize(1); /** * remove lines in first block */ m_blocks.last()->clearLines(); m_lines = 0; /** * try to open file, with given encoding * in round 0 + 3 use the given encoding from user * in round 1 use 0, to trigger detection * in round 2 use fallback */ QTextCodec *codec = m_textCodec; if (i == 1) { codec = nullptr; } else if (i == 2) { codec = m_fallbackTextCodec; } if (!file.open(codec)) { // create one dummy textline, in any case m_blocks.last()->appendLine(QString()); m_lines++; return false; } // read in all lines... encodingErrors = false; while (!file.eof()) { // read line int offset = 0, length = 0; bool currentError = !file.readLine(offset, length); encodingErrors = encodingErrors || currentError; // bail out on encoding error, if not last round! if (encodingErrors && i < (enforceTextCodec ? 0 : 3)) { BUFFER_DEBUG << "Failed try to load file" << filename << "with codec" << (file.textCodec() ? file.textCodec()->name() : "(null)"); break; } // get Unicode data for this line const QChar *unicodeData = file.unicode() + offset; if (longestLineLoaded < length) longestLineLoaded=length; /** * split lines, if too large */ do { /** * calculate line length */ int lineLength = length; if ((m_lineLengthLimit > 0) && (lineLength > m_lineLengthLimit)) { /** * search for place to wrap */ int spacePosition = m_lineLengthLimit - 1; for (int testPosition = m_lineLengthLimit - 1; (testPosition >= 0) && (testPosition >= (m_lineLengthLimit - (m_lineLengthLimit / 10))); --testPosition) { /** * wrap place found? */ if (unicodeData[testPosition].isSpace() || unicodeData[testPosition].isPunct()) { spacePosition = testPosition; break; } } /** * wrap the line */ lineLength = spacePosition + 1; length -= lineLength; tooLongLinesWrapped = true; } else { /** * be done after this round */ length = 0; } /** * construct new text line with content from file * move data pointer */ QString textLine(unicodeData, lineLength); unicodeData += lineLength; /** * ensure blocks aren't too large */ if (m_blocks.last()->lines() >= m_blockSize) { m_blocks.append(new TextBlock(this, m_blocks.last()->startLine() + m_blocks.last()->lines())); } /** * append line to last block */ m_blocks.last()->appendLine(textLine); ++m_lines; } while (length > 0); } // if no encoding error, break out of reading loop if (!encodingErrors) { // remember used codec, might change bom setting setTextCodec(file.textCodec()); break; } } // save checksum of file on disk setDigest(file.digest()); // remember if BOM was found if (file.byteOrderMarkFound()) { setGenerateByteOrderMark(true); } // remember eol mode, if any found in file if (file.eol() != eolUnknown) { setEndOfLineMode(file.eol()); } // remember mime type for filter device m_mimeTypeForFilterDev = file.mimeTypeForFilterDev(); // assert that one line is there! Q_ASSERT(m_lines > 0); // report CODEC + ERRORS BUFFER_DEBUG << "Loaded file " << filename << "with codec" << m_textCodec->name() << (encodingErrors ? "with" : "without") << "encoding errors"; // report BOM BUFFER_DEBUG << (file.byteOrderMarkFound() ? "Found" : "Didn't find") << "byte order mark"; // report filter device mime-type BUFFER_DEBUG << "used filter device for mime-type" << m_mimeTypeForFilterDev; // emit success emit loaded(filename, encodingErrors); // file loading worked, modulo encoding problems return true; } const QByteArray &TextBuffer::digest() const { return m_digest; } void TextBuffer::setDigest(const QByteArray &checksum) { m_digest = checksum; } void TextBuffer::setTextCodec(QTextCodec *codec) { m_textCodec = codec; // enforce bom for some encodings int mib = m_textCodec->mibEnum(); if (mib == 1013 || mib == 1014 || mib == 1015) { // utf16 setGenerateByteOrderMark(true); } if (mib == 1017 || mib == 1018 || mib == 1019) { // utf32 setGenerateByteOrderMark(true); } } bool TextBuffer::save(const QString &filename) { /** * codec must be set, else below we fail! */ Q_ASSERT(m_textCodec); SaveResult saveRes = saveBufferUnprivileged(filename); if (saveRes == SaveResult::Failed) { return false; } else if (saveRes == SaveResult::MissingPermissions) { /** * either unit-test mode or we're missing permissions to write to the * file => use temporary file and try to use authhelper */ if (!saveBufferEscalated(filename)) { return false; } } // remember this revision as last saved m_history.setLastSavedRevision(); // inform that we have saved the state markModifiedLinesAsSaved(); // emit that file was saved and be done emit saved(filename); return true; } bool TextBuffer::saveBuffer(const QString &filename, KCompressionDevice &saveFile) { /** * construct stream + disable Unicode headers */ QTextStream stream(&saveFile); stream.setCodec(QTextCodec::codecForName("UTF-16")); // set the correct codec stream.setCodec(m_textCodec); // generate byte order mark? stream.setGenerateByteOrderMark(generateByteOrderMark()); // our loved eol string ;) QString eol = QStringLiteral("\n"); if (endOfLineMode() == eolDos) { eol = QStringLiteral("\r\n"); } else if (endOfLineMode() == eolMac) { eol = QStringLiteral("\r"); } // just dump the lines out ;) for (int i = 0; i < m_lines; ++i) { // dump current line stream << line(i)->text(); // append correct end of line string if ((i + 1) < m_lines) { stream << eol; } // early out on stream errors if (stream.status() != QTextStream::Ok) { return false; } } // do we need to add a trailing newline char? if (m_newLineAtEof) { Q_ASSERT(m_lines > 0); // see .h file const Kate::TextLine lastLine = line(m_lines - 1); const int firstChar = lastLine->firstChar(); if (firstChar > -1 || lastLine->length() > 0) { stream << eol; } } // flush stream stream.flush(); // only finalize if stream status == OK if (stream.status() != QTextStream::Ok) { return false; } // close the file, we might want to read from underlying buffer below saveFile.close(); // did save work? if (saveFile.error() != QFileDevice::NoError) { BUFFER_DEBUG << "Saving file " << filename << "failed with error" << saveFile.errorString(); return false; } return true; } TextBuffer::SaveResult TextBuffer::saveBufferUnprivileged(const QString &filename) { if (m_alwaysUseKAuthForSave) { // unit-testing mode, simulate we need privileges return SaveResult::MissingPermissions; } /** * construct correct filter device * we try to use the same compression as for opening */ const KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(m_mimeTypeForFilterDev); QScopedPointer saveFile(new KCompressionDevice(filename, type)); if (!saveFile->open(QIODevice::WriteOnly)) { #ifdef CAN_USE_ERRNO if (errno != EACCES) { return SaveResult::Failed; } #endif return SaveResult::MissingPermissions; } if (!saveBuffer(filename, *saveFile)) { return SaveResult::Failed; } return SaveResult::Success; } bool TextBuffer::saveBufferEscalated(const QString &filename) { /** * construct correct filter device * we try to use the same compression as for opening */ const KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType(m_mimeTypeForFilterDev); QScopedPointer saveFile(new KCompressionDevice(filename, type)); uint ownerId = -2; uint groupId = -2; QScopedPointer temporaryBuffer; /** * Memorize owner and group. */ const QFileInfo fileInfo(filename); if (fileInfo.exists()) { ownerId = fileInfo.ownerId(); groupId = fileInfo.groupId(); } // if that fails we need more privileges to save this file // -> we write to a temporary file and then send its path to KAuth action for privileged save temporaryBuffer.reset(new QBuffer()); // open buffer for write and read (read is used for checksum computing and writing to temporary file) if (!temporaryBuffer->open(QIODevice::ReadWrite)) { return false; } // we are now saving to a temporary buffer with potential compression proxy saveFile.reset(new KCompressionDevice(temporaryBuffer.data(), false, type)); if (!saveFile->open(QIODevice::WriteOnly)) { return false; } if( !saveBuffer(filename, *saveFile) ) { return false; } // temporary buffer was used to save the file // -> computing checksum // -> saving to temporary file // -> copying the temporary file to the original file location with KAuth action QTemporaryFile tempFile; if (!tempFile.open()) { return false; } // go to QBuffer start temporaryBuffer->seek(0); // read contents of QBuffer and add them to checksum utility as well as to QTemporaryFile char buffer[bufferLength]; qint64 read = -1; QCryptographicHash cryptographicHash(SecureTextBuffer::checksumAlgorithm); while ((read = temporaryBuffer->read(buffer, bufferLength)) > 0) { cryptographicHash.addData(buffer, read); if (tempFile.write(buffer, read) == -1) { return false; } } if (!tempFile.flush()) { return false; } // prepare data for KAuth action QVariantMap kAuthActionArgs; - kAuthActionArgs.insert(QLatin1String("sourceFile"), tempFile.fileName()); - kAuthActionArgs.insert(QLatin1String("targetFile"), filename); - kAuthActionArgs.insert(QLatin1String("checksum"), cryptographicHash.result()); - kAuthActionArgs.insert(QLatin1String("ownerId"), ownerId); - kAuthActionArgs.insert(QLatin1String("groupId"), groupId); + kAuthActionArgs.insert(QStringLiteral("sourceFile"), tempFile.fileName()); + kAuthActionArgs.insert(QStringLiteral("targetFile"), filename); + kAuthActionArgs.insert(QStringLiteral("checksum"), cryptographicHash.result()); + kAuthActionArgs.insert(QStringLiteral("ownerId"), ownerId); + kAuthActionArgs.insert(QStringLiteral("groupId"), groupId); // call save with elevated privileges if (KTextEditor::EditorPrivate::unitTestMode()) { // unit testing purposes only if (!SecureTextBuffer::savefile(kAuthActionArgs).succeeded()) { return false; } } else { - KAuth::Action kAuthSaveAction(QLatin1String("org.kde.ktexteditor.katetextbuffer.savefile")); - kAuthSaveAction.setHelperId(QLatin1String("org.kde.ktexteditor.katetextbuffer")); + KAuth::Action kAuthSaveAction(QStringLiteral("org.kde.ktexteditor.katetextbuffer.savefile")); + kAuthSaveAction.setHelperId(QStringLiteral("org.kde.ktexteditor.katetextbuffer")); kAuthSaveAction.setArguments(kAuthActionArgs); KAuth::ExecuteJob *job = kAuthSaveAction.execute(); if (!job->exec()) { return false; } } return true; } void TextBuffer::notifyAboutRangeChange(KTextEditor::View *view, int startLine, int endLine, bool rangeWithAttribute) { /** * ignore calls if no document is around */ if (!m_document) { return; } /** * update all views, this IS ugly and could be a signal, but I profiled and a signal is TOO slow, really * just create 20k ranges in a go and you wait seconds on a decent machine */ const QList &views = m_document->views(); foreach (KTextEditor::View *curView, views) { // filter wrong views if (view && view != curView) { continue; } // notify view, it is really a kate view static_cast(curView)->notifyAboutRangeChange(startLine, endLine, rangeWithAttribute); } } void TextBuffer::markModifiedLinesAsSaved() { foreach (TextBlock *block, m_blocks) { block->markModifiedLinesAsSaved(); } } QList TextBuffer::rangesForLine(int line, KTextEditor::View *view, bool rangesWithAttributeOnly) const { // get block, this will assert on invalid line const int blockIndex = blockForLine(line); // get the ranges of the right block QList rightRanges; foreach (const QSet &ranges, m_blocks.at(blockIndex)->rangesForLine(line)) { foreach (TextRange *const range, ranges) { /** * we want only ranges with attributes, but this one has none */ if (rangesWithAttributeOnly && !range->hasAttribute()) { continue; } /** * we want ranges for no view, but this one's attribute is only valid for views */ if (!view && range->attributeOnlyForViews()) { continue; } /** * the range's attribute is not valid for this view */ if (range->view() && range->view() != view) { continue; } /** * if line is in the range, ok */ if (range->startInternal().lineInternal() <= line && line <= range->endInternal().lineInternal()) { rightRanges.append(range); } } } // return right ranges return rightRanges; } } diff --git a/src/buffer/katetextfolding.cpp b/src/buffer/katetextfolding.cpp index 5c29d804..6caf96dd 100644 --- a/src/buffer/katetextfolding.cpp +++ b/src/buffer/katetextfolding.cpp @@ -1,1030 +1,1030 @@ /* This file is part of the Kate project. * * Copyright (C) 2013 Christoph Cullmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katetextfolding.h" #include "katetextbuffer.h" #include "katetextrange.h" #include "documentcursor.h" #include namespace Kate { TextFolding::FoldingRange::FoldingRange(TextBuffer &buffer, const KTextEditor::Range &range, FoldingRangeFlags _flags) : start(new TextCursor(buffer, range.start(), KTextEditor::MovingCursor::MoveOnInsert)) , end(new TextCursor(buffer, range.end(), KTextEditor::MovingCursor::MoveOnInsert)) , parent(nullptr) , flags(_flags) , id(-1) { } TextFolding::FoldingRange::~FoldingRange() { /** * kill all our data! * this will recurse all sub-structures! */ delete start; delete end; qDeleteAll(nestedRanges); } TextFolding::TextFolding(TextBuffer &buffer) : QObject() , m_buffer(buffer) , m_idCounter(-1) { /** * connect needed signals from buffer */ connect(&m_buffer, SIGNAL(cleared()), SLOT(clear())); } TextFolding::~TextFolding() { /** * only delete the folding ranges, the folded ranges and mapped ranges are the same objects */ qDeleteAll(m_foldingRanges); } void TextFolding::clear() { /** * reset counter */ m_idCounter = -1; /** * no ranges, no work */ if (m_foldingRanges.isEmpty()) { /** * assert all stuff is consistent and return! */ Q_ASSERT(m_idToFoldingRange.isEmpty()); Q_ASSERT(m_foldedFoldingRanges.isEmpty()); return; } /** * cleanup */ m_idToFoldingRange.clear(); m_foldedFoldingRanges.clear(); qDeleteAll(m_foldingRanges); m_foldingRanges.clear(); /** * folding changed! */ emit foldingRangesChanged(); } qint64 TextFolding::newFoldingRange(const KTextEditor::Range &range, FoldingRangeFlags flags) { /** * sort out invalid and empty ranges * that makes no sense, they will never grow again! */ if (!range.isValid() || range.isEmpty()) { return -1; } /** * create new folding region that we want to insert * this will internally create moving cursors! */ FoldingRange *newRange = new FoldingRange(m_buffer, range, flags); /** * the construction of the text cursors might have invalidated this * check and bail out if that happens * bail out, too, if it can't be inserted! */ if (!newRange->start->isValid() || !newRange->end->isValid() || !insertNewFoldingRange(nullptr /* no parent here */, m_foldingRanges, newRange)) { /** * cleanup and be done */ delete newRange; return -1; } /** * set id, catch overflows, even if they shall not happen */ newRange->id = ++m_idCounter; if (newRange->id < 0) { newRange->id = m_idCounter = 0; } /** * remember the range */ m_idToFoldingRange.insert(newRange->id, newRange); /** * update our folded ranges vector! */ bool updated = updateFoldedRangesForNewRange(newRange); /** * emit that something may have changed * do that only, if updateFoldedRangesForNewRange did not already do the job! */ if (!updated) { emit foldingRangesChanged(); } /** * all went fine, newRange is now registered internally! */ return newRange->id; } KTextEditor::Range TextFolding::foldingRange(qint64 id) const { FoldingRange *range = m_idToFoldingRange.value(id); if (!range) { return KTextEditor::Range::invalid(); } return KTextEditor::Range(range->start->toCursor(), range->end->toCursor()); } bool TextFolding::foldRange(qint64 id) { /** * try to find the range, else bail out */ FoldingRange *range = m_idToFoldingRange.value(id); if (!range) { return false; } /** * already folded? nothing to do */ if (range->flags & Folded) { return true; } /** * fold and be done */ range->flags |= Folded; updateFoldedRangesForNewRange(range); return true; } bool TextFolding::unfoldRange(qint64 id, bool remove) { /** * try to find the range, else bail out */ FoldingRange *range = m_idToFoldingRange.value(id); if (!range) { return false; } /** * nothing to do? * range is already unfolded and we need not to remove it! */ if (!remove && !(range->flags & Folded)) { return true; } /** * do we need to delete the range? */ const bool deleteRange = remove || !(range->flags & Persistent); /** * first: remove the range, if forced or non-persistent! */ if (deleteRange) { /** * remove from outside visible mapping! */ m_idToFoldingRange.remove(id); /** * remove from folding vectors! * FIXME: OPTIMIZE */ FoldingRange::Vector &parentVector = range->parent ? range->parent->nestedRanges : m_foldingRanges; FoldingRange::Vector newParentVector; Q_FOREACH (FoldingRange *curRange, parentVector) { /** * insert our nested ranges and reparent them */ if (curRange == range) { Q_FOREACH (FoldingRange *newRange, range->nestedRanges) { newRange->parent = range->parent; newParentVector.push_back(newRange); } continue; } /** * else just transfer elements */ newParentVector.push_back(curRange); } parentVector = newParentVector; } /** * second: unfold the range, if needed! */ bool updated = false; if (range->flags & Folded) { range->flags &= ~Folded; updated = updateFoldedRangesForRemovedRange(range); } /** * emit that something may have changed * do that only, if updateFoldedRangesForRemoveRange did not already do the job! */ if (!updated) { emit foldingRangesChanged(); } /** * really delete the range, if needed! */ if (deleteRange) { /** * clear ranges first, they got moved! */ range->nestedRanges.clear(); delete range; } /** * be done ;) */ return true; } bool TextFolding::isLineVisible(int line, qint64 *foldedRangeId) const { /** * skip if nothing folded */ if (m_foldedFoldingRanges.isEmpty()) { return true; } /** * search upper bound, index to item with start line higher than our one */ FoldingRange::Vector::const_iterator upperBound = std::upper_bound(m_foldedFoldingRanges.begin(), m_foldedFoldingRanges.end(), line, compareRangeByStartWithLine); if (upperBound != m_foldedFoldingRanges.begin()) { --upperBound; } /** * check if we overlap with the range in front of us */ const bool hidden = (((*upperBound)->end->line() >= line) && (line > (*upperBound)->start->line())); /** * fill in folded range id, if needed */ if (foldedRangeId) { (*foldedRangeId) = hidden ? (*upperBound)->id : -1; } /** * visible == !hidden */ return !hidden; } void TextFolding::ensureLineIsVisible(int line) { /** * skip if nothing folded */ if (m_foldedFoldingRanges.isEmpty()) { return; } /** * while not visible, unfold */ qint64 foldedRangeId = -1; while (!isLineVisible(line, &foldedRangeId)) { /** * id should be valid! */ Q_ASSERT(foldedRangeId >= 0); /** * unfold shall work! */ const bool unfolded = unfoldRange(foldedRangeId); (void) unfolded; Q_ASSERT(unfolded); } } int TextFolding::visibleLines() const { /** * start with all lines we have */ int visibleLines = m_buffer.lines(); /** * skip if nothing folded */ if (m_foldedFoldingRanges.isEmpty()) { return visibleLines; } /** * count all folded lines and subtract them from visible lines */ Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { visibleLines -= (range->end->line() - range->start->line()); } /** * be done, assert we did no trash */ Q_ASSERT(visibleLines > 0); return visibleLines; } int TextFolding::lineToVisibleLine(int line) const { /** * valid input needed! */ Q_ASSERT(line >= 0); /** * start with identity */ int visibleLine = line; /** * skip if nothing folded or first line */ if (m_foldedFoldingRanges.isEmpty() || (line == 0)) { return visibleLine; } /** * walk over all folded ranges until we reach the line * keep track of seen visible lines, for the case we want to convert a hidden line! */ int seenVisibleLines = 0; int lastLine = 0; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * abort if we reach our line! */ if (range->start->line() >= line) { break; } /** * count visible lines */ seenVisibleLines += (range->start->line() - lastLine); lastLine = range->end->line(); /** * we might be contained in the region, then we return last visible line */ if (line <= range->end->line()) { return seenVisibleLines; } /** * subtrace folded lines */ visibleLine -= (range->end->line() - range->start->line()); } /** * be done, assert we did no trash */ Q_ASSERT(visibleLine >= 0); return visibleLine; } int TextFolding::visibleLineToLine(int visibleLine) const { /** * valid input needed! */ Q_ASSERT(visibleLine >= 0); /** * start with identity */ int line = visibleLine; /** * skip if nothing folded or first line */ if (m_foldedFoldingRanges.isEmpty() || (visibleLine == 0)) { return line; } /** * last visible line seen, as line in buffer */ int seenVisibleLines = 0; int lastLine = 0; int lastLineVisibleLines = 0; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * else compute visible lines and move last seen */ lastLineVisibleLines = seenVisibleLines; seenVisibleLines += (range->start->line() - lastLine); /** * bail out if enough seen */ if (seenVisibleLines >= visibleLine) { break; } lastLine = range->end->line(); } /** * check if still no enough visible! */ if (seenVisibleLines < visibleLine) { lastLineVisibleLines = seenVisibleLines; } /** * compute visible line */ line = (lastLine + (visibleLine - lastLineVisibleLines)); Q_ASSERT(line >= 0); return line; } QVector > TextFolding::foldingRangesStartingOnLine(int line) const { /** * results vector */ QVector > results; /** * recursively do binary search */ foldingRangesStartingOnLine(results, m_foldingRanges, line); /** * return found results */ return results; } void TextFolding::foldingRangesStartingOnLine(QVector > &results, const TextFolding::FoldingRange::Vector &ranges, int line) const { /** * early out for no folds */ if (ranges.isEmpty()) { return; } /** * first: lower bound of start */ FoldingRange::Vector::const_iterator lowerBound = std::lower_bound(ranges.begin(), ranges.end(), line, compareRangeByLineWithStart); /** * second: upper bound of end */ FoldingRange::Vector::const_iterator upperBound = std::upper_bound(ranges.begin(), ranges.end(), line, compareRangeByStartWithLine); /** * we may need to go one to the left, if not already at the begin, as we might overlap with the one in front of us! */ if ((lowerBound != ranges.begin()) && ((*(lowerBound - 1))->end->line() >= line)) { --lowerBound; } /** * for all of them, check if we start at right line and recurse */ for (FoldingRange::Vector::const_iterator it = lowerBound; it != upperBound; ++it) { /** * this range already ok? add it to results */ if ((*it)->start->line() == line) { results.push_back(qMakePair((*it)->id, (*it)->flags)); } /** * recurse anyway */ foldingRangesStartingOnLine(results, (*it)->nestedRanges, line); } } QVector > TextFolding::foldingRangesForParentRange(qint64 parentRangeId) const { /** * toplevel ranges requested or real parent? */ const FoldingRange::Vector *ranges = nullptr; if (parentRangeId == -1) { ranges = &m_foldingRanges; } else if (FoldingRange *range = m_idToFoldingRange.value(parentRangeId)) { ranges = &range->nestedRanges; } /** * no ranges => nothing to do */ QVector > results; if (!ranges) return results; /** * else convert ranges to id + flags and pass that back */ for (FoldingRange::Vector::const_iterator it = ranges->begin(); it != ranges->end(); ++it) { results.append(qMakePair((*it)->id, (*it)->flags)); } return results; } QString TextFolding::debugDump() const { /** * dump toplevel ranges recursively */ return QStringLiteral("tree %1 - folded %2").arg(debugDump(m_foldingRanges, true), debugDump(m_foldedFoldingRanges, false)); } void TextFolding::debugPrint(const QString &title) const { // print title + content printf("%s\n %s\n", qPrintable(title), qPrintable(debugDump())); } QString TextFolding::debugDump(const TextFolding::FoldingRange::Vector &ranges, bool recurse) { /** * dump all ranges recursively */ QString dump; Q_FOREACH (FoldingRange *range, ranges) { if (!dump.isEmpty()) { dump += QLatin1Char(' '); } const QString persistent = (range->flags & Persistent) ? QStringLiteral("p") : QString(); const QString folded = (range->flags & Folded) ? QStringLiteral("f") : QString(); - dump += QString::fromLatin1("[%1:%2 %3%4 ").arg(range->start->line()).arg(range->start->column()).arg(persistent, folded); + dump += QStringLiteral("[%1:%2 %3%4 ").arg(range->start->line()).arg(range->start->column()).arg(persistent, folded); /** * recurse */ if (recurse) { QString inner = debugDump(range->nestedRanges, recurse); if (!inner.isEmpty()) { dump += inner + QLatin1Char(' '); } } - dump += QString::fromLatin1("%1:%2]").arg(range->end->line()).arg(range->end->column()); + dump += QStringLiteral("%1:%2]").arg(range->end->line()).arg(range->end->column()); } return dump; } bool TextFolding::insertNewFoldingRange(FoldingRange *parent, FoldingRange::Vector &existingRanges, FoldingRange *newRange) { /** * existing ranges are non-overlapping and sorted * that means, we can search for lower bound of start of range and upper bound of end of range to find all "overlapping" ranges. */ /** * first: lower bound of start */ FoldingRange::Vector::iterator lowerBound = std::lower_bound(existingRanges.begin(), existingRanges.end(), newRange, compareRangeByStart); /** * second: upper bound of end */ FoldingRange::Vector::iterator upperBound = std::upper_bound(existingRanges.begin(), existingRanges.end(), newRange, compareRangeByEnd); /** * we may need to go one to the left, if not already at the begin, as we might overlap with the one in front of us! */ if ((lowerBound != existingRanges.begin()) && ((*(lowerBound - 1))->end->toCursor() > newRange->start->toCursor())) { --lowerBound; } /** * now: first case, we overlap with nothing or hit exactly one range! */ if (lowerBound == upperBound) { /** * nothing we overlap with? * then just insert and be done! */ if ((lowerBound == existingRanges.end()) || (newRange->start->toCursor() >= (*lowerBound)->end->toCursor()) || (newRange->end->toCursor() <= (*lowerBound)->start->toCursor())) { /** * insert + fix parent */ existingRanges.insert(lowerBound, newRange); newRange->parent = parent; /** * all done */ return true; } /** * we are contained in this one range? * then recurse! */ if ((newRange->start->toCursor() >= (*lowerBound)->start->toCursor()) && (newRange->end->toCursor() <= (*lowerBound)->end->toCursor())) { return insertNewFoldingRange((*lowerBound), (*lowerBound)->nestedRanges, newRange); } /** * else: we might contain at least this fold, or many more, if this if block is not taken at all * use the general code that checks for "we contain stuff" below! */ } /** * check if we contain other folds! */ FoldingRange::Vector::iterator it = lowerBound; bool includeUpperBound = false; FoldingRange::Vector nestedRanges; while (it != existingRanges.end()) { /** * do we need to take look at upper bound, too? * if not break */ if (it == upperBound) { if (newRange->end->toCursor() <= (*upperBound)->start->toCursor()) { break; } else { includeUpperBound = true; } } /** * if one region is not contained in the new one, abort! * then this is not well nested! */ if (!((newRange->start->toCursor() <= (*it)->start->toCursor()) && (newRange->end->toCursor() >= (*it)->end->toCursor()))) { return false; } /** * include into new nested ranges */ nestedRanges.push_back((*it)); /** * end reached */ if (it == upperBound) { break; } /** * else increment */ ++it; } /** * if we arrive here, all is nicely nested into our new range * remove the contained ones here, insert new range with new nested ranges we already constructed */ it = existingRanges.erase(lowerBound, includeUpperBound ? (upperBound + 1) : upperBound); existingRanges.insert(it, newRange); newRange->nestedRanges = nestedRanges; /** * correct parent mapping! */ newRange->parent = parent; Q_FOREACH (FoldingRange *range, newRange->nestedRanges) { range->parent = newRange; } /** * all nice */ return true; } bool TextFolding::compareRangeByStart(FoldingRange *a, FoldingRange *b) { return a->start->toCursor() < b->start->toCursor(); } bool TextFolding::compareRangeByEnd(FoldingRange *a, FoldingRange *b) { return a->end->toCursor() < b->end->toCursor(); } bool TextFolding::compareRangeByStartWithLine(int line, FoldingRange *range) { return (line < range->start->line()); } bool TextFolding::compareRangeByLineWithStart(FoldingRange *range, int line) { return (range->start->line() < line); } bool TextFolding::updateFoldedRangesForNewRange(TextFolding::FoldingRange *newRange) { /** * not folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector */ if (!(newRange->flags & Folded)) { return false; } /** * any of the parents folded? not interesting, too! */ TextFolding::FoldingRange *parent = newRange->parent; while (parent) { /** * parent folded => be done */ if (parent->flags & Folded) { return false; } /** * walk up */ parent = parent->parent; } /** * ok, if we arrive here, we are a folded range and we have no folded parent * we now want to add this range to the m_foldedFoldingRanges vector, just removing any ranges that is included in it! * TODO: OPTIMIZE */ FoldingRange::Vector newFoldedFoldingRanges; bool newRangeInserted = false; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * contained? kill */ if ((newRange->start->toCursor() <= range->start->toCursor()) && (newRange->end->toCursor() >= range->end->toCursor())) { continue; } /** * range is behind newRange? * insert newRange if not already done */ if (!newRangeInserted && (range->start->toCursor() >= newRange->end->toCursor())) { newFoldedFoldingRanges.push_back(newRange); newRangeInserted = true; } /** * just transfer range */ newFoldedFoldingRanges.push_back(range); } /** * last: insert new range, if not done */ if (!newRangeInserted) { newFoldedFoldingRanges.push_back(newRange); } /** * fixup folded ranges */ m_foldedFoldingRanges = newFoldedFoldingRanges; /** * folding changed! */ emit foldingRangesChanged(); /** * all fine, stuff done, signal emitted */ return true; } bool TextFolding::updateFoldedRangesForRemovedRange(TextFolding::FoldingRange *oldRange) { /** * folded? not interesting! we don't need to touch our m_foldedFoldingRanges vector */ if (oldRange->flags & Folded) { return false; } /** * any of the parents folded? not interesting, too! */ TextFolding::FoldingRange *parent = oldRange->parent; while (parent) { /** * parent folded => be done */ if (parent->flags & Folded) { return false; } /** * walk up */ parent = parent->parent; } /** * ok, if we arrive here, we are a unfolded range and we have no folded parent * we now want to remove this range from the m_foldedFoldingRanges vector and include our nested folded ranges! * TODO: OPTIMIZE */ FoldingRange::Vector newFoldedFoldingRanges; Q_FOREACH (FoldingRange *range, m_foldedFoldingRanges) { /** * right range? insert folded nested ranges */ if (range == oldRange) { appendFoldedRanges(newFoldedFoldingRanges, oldRange->nestedRanges); continue; } /** * just transfer range */ newFoldedFoldingRanges.push_back(range); } /** * fixup folded ranges */ m_foldedFoldingRanges = newFoldedFoldingRanges; /** * folding changed! */ emit foldingRangesChanged(); /** * all fine, stuff done, signal emitted */ return true; } void TextFolding::appendFoldedRanges(TextFolding::FoldingRange::Vector &newFoldedFoldingRanges, const TextFolding::FoldingRange::Vector &ranges) const { /** * search for folded ranges and append them */ Q_FOREACH (FoldingRange *range, ranges) { /** * itself folded? append */ if (range->flags & Folded) { newFoldedFoldingRanges.push_back(range); continue; } /** * else: recurse! */ appendFoldedRanges(newFoldedFoldingRanges, range->nestedRanges); } } QJsonDocument TextFolding::exportFoldingRanges() const { QJsonArray array; exportFoldingRanges(m_foldingRanges, array); QJsonDocument folds; folds.setArray(array); return folds; } void TextFolding::exportFoldingRanges(const TextFolding::FoldingRange::Vector &ranges, QJsonArray &folds) { /** * dump all ranges recursively */ Q_FOREACH (FoldingRange *range, ranges) { /** * construct one range and dump to folds */ QJsonObject rangeMap; rangeMap[QStringLiteral("startLine")] = range->start->line(); rangeMap[QStringLiteral("startColumn")] = range->start->column(); rangeMap[QStringLiteral("endLine")] = range->end->line(); rangeMap[QStringLiteral("endColumn")] = range->end->column(); rangeMap[QStringLiteral("flags")] = (int)range->flags; folds.append(rangeMap); /** * recurse */ exportFoldingRanges(range->nestedRanges, folds); } } void TextFolding::importFoldingRanges(const QJsonDocument &folds) { Q_FOREACH (FoldingRange *range, m_foldingRanges) { unfoldRange(range->id); } /** * try to create all folding ranges */ Q_FOREACH (const QJsonValue &rangeVariant, folds.array()) { /** * get map */ QJsonObject rangeMap = rangeVariant.toObject(); /** * construct range start/end */ const KTextEditor::Cursor start(rangeMap[QStringLiteral("startLine")].toInt(), rangeMap[QStringLiteral("startColumn")].toInt()); const KTextEditor::Cursor end(rangeMap[QStringLiteral("endLine")].toInt(), rangeMap[QStringLiteral("endColumn")].toInt()); /** * check validity (required when loading a possibly broken folding state from disk) */ if (start >= end || (m_buffer.document() && // <-- unit test katetextbuffertest does not have a KTE::Document assigned (!KTextEditor::DocumentCursor(m_buffer.document(), start).isValidTextPosition() || !KTextEditor::DocumentCursor(m_buffer.document(), end).isValidTextPosition())) ) { continue; } /** * get flags */ const int rawFlags = rangeMap[QStringLiteral("flags")].toInt(); FoldingRangeFlags flags; if (rawFlags & Persistent) { flags = Persistent; } if (rawFlags & Folded) { flags = Folded; } /** * create folding range */ newFoldingRange(KTextEditor::Range(start, end), flags); } } } diff --git a/src/completion/katecompletionconfig.cpp b/src/completion/katecompletionconfig.cpp index 7e73f8e8..67508bd5 100644 --- a/src/completion/katecompletionconfig.cpp +++ b/src/completion/katecompletionconfig.cpp @@ -1,448 +1,448 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2006 Hamish Rodda * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katecompletionconfig.h" #include "katecompletionmodel.h" #include "kateglobal.h" #include "ui_completionconfigwidget.h" #include #include #include #include #include #include #include using namespace KTextEditor; KateCompletionConfig::KateCompletionConfig(KateCompletionModel *model, QWidget *parent) : QDialog(parent) , ui(new Ui::CompletionConfigWidget()) , m_model(model) { setWindowTitle(i18n("Code Completion Configuration")); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); QWidget *mw = new QWidget(this); mainLayout->addWidget(mw); ui->setupUi(mw); // Sorting ui->sorting->setChecked(m_model->isSortingEnabled()); ui->sortingAlphabetical->setChecked(m_model->isSortingAlphabetical()); ui->sortingCaseSensitive->setChecked(m_model->sortingCaseSensitivity() == Qt::CaseSensitive); ui->groupingOrderUp->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); ui->groupingOrderDown->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); connect(ui->groupingOrderUp, SIGNAL(pressed()), SLOT(moveGroupingOrderUp())); connect(ui->groupingOrderDown, SIGNAL(pressed()), SLOT(moveGroupingOrderDown())); // Filtering ui->filtering->setChecked(m_model->isFilteringEnabled()); ui->filteringContextMatchOnly->setChecked(m_model->filterContextMatchesOnly()); ui->filteringHideAttributes->setChecked(m_model->filterByAttribute()); for (CodeCompletionModel::CompletionProperty i = CodeCompletionModel::FirstProperty; i <= CodeCompletionModel::LastProperty; i = static_cast(i << 1)) { QListWidgetItem *item = new QListWidgetItem(m_model->propertyName(i), ui->filteringAttributesList, i); item->setCheckState((m_model->filterAttributes() & i) ? Qt::Checked : Qt::Unchecked); } ui->filteringMaximumInheritanceDepth->setValue(m_model->maximumInheritanceDepth()); // Grouping ui->grouping->setChecked(m_model->isGroupingEnabled()); ui->groupingUp->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); ui->groupingDown->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_groupingScopeType = ui->groupingMethods->topLevelItem(0); m_groupingScopeType->setCheckState(0, (m_model->groupingMethod() & KateCompletionModel::ScopeType) ? Qt::Checked : Qt::Unchecked); m_groupingScope = ui->groupingMethods->topLevelItem(1); m_groupingScope->setCheckState(0, (m_model->groupingMethod() & KateCompletionModel::Scope) ? Qt::Checked : Qt::Unchecked); m_groupingAccessType = ui->groupingMethods->topLevelItem(2); m_groupingAccessType->setCheckState(0, (m_model->groupingMethod() & KateCompletionModel::AccessType) ? Qt::Checked : Qt::Unchecked); m_groupingItemType = ui->groupingMethods->topLevelItem(3); m_groupingItemType->setCheckState(0, (m_model->groupingMethod() & KateCompletionModel::ItemType) ? Qt::Checked : Qt::Unchecked); ui->accessConst->setChecked(m_model->accessIncludeConst()); ui->accessStatic->setChecked(m_model->accessIncludeStatic()); ui->accessSignalSlot->setChecked(m_model->accessIncludeSignalSlot()); for (int i = 0; i < 4; ++i) { ui->groupingMethods->topLevelItem(i)->setCheckState(0, Qt::Unchecked); } connect(ui->groupingUp, SIGNAL(pressed()), SLOT(moveGroupingUp())); connect(ui->groupingDown, SIGNAL(pressed()), SLOT(moveGroupingDown())); // Column merging ui->columnMerging->setChecked(m_model->isColumnMergingEnabled()); ui->columnUp->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); ui->columnDown->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); connect(ui->columnUp, SIGNAL(pressed()), SLOT(moveColumnUp())); connect(ui->columnDown, SIGNAL(pressed()), SLOT(moveColumnDown())); QList mergedColumns; if (!m_model->columnMerges().isEmpty()) { foreach (const QList &list, m_model->columnMerges()) { bool first = true; foreach (int column, list) { QTreeWidgetItem *item = new QTreeWidgetItem(ui->columnMergeTree, column); - item->setText(0, KateCompletionModel::columnName(column) + QString::fromLatin1(" %1").arg(column)); + item->setText(0, KateCompletionModel::columnName(column) + QStringLiteral(" %1").arg(column)); item->setCheckState(1, first ? Qt::Unchecked : Qt::Checked); if (column == KTextEditor::CodeCompletionModel::Name) { item->setText(2, i18n("Always")); } else { item->setCheckState(2, Qt::Checked); } first = false; mergedColumns << column; } } for (int column = 0; column < KTextEditor::CodeCompletionModel::ColumnCount; ++column) { if (!mergedColumns.contains(column)) { QTreeWidgetItem *item = new QTreeWidgetItem(ui->columnMergeTree, column); - item->setText(0, KateCompletionModel::columnName(column) + QString::fromLatin1(" %1").arg(column)); + item->setText(0, KateCompletionModel::columnName(column) + QStringLiteral(" %1").arg(column)); item->setCheckState(1, Qt::Unchecked); Q_ASSERT(column != KTextEditor::CodeCompletionModel::Name); item->setCheckState(2, Qt::Unchecked); } } } else { for (int column = 0; column < KTextEditor::CodeCompletionModel::ColumnCount; ++column) { QTreeWidgetItem *item = new QTreeWidgetItem(ui->columnMergeTree, column); - item->setText(0, KateCompletionModel::columnName(column) + QString::fromLatin1(" %1").arg(column)); + item->setText(0, KateCompletionModel::columnName(column) + QStringLiteral(" %1").arg(column)); item->setCheckState(1, Qt::Unchecked); if (column == KTextEditor::CodeCompletionModel::Name) { item->setText(2, i18n("Always")); } else { item->setCheckState(2, Qt::Checked); } } } // init with defaults from config or really hardcoded ones KConfigGroup config(KTextEditor::EditorPrivate::config(), "Code Completion"); readConfig(config); // buttons QDialogButtonBox *buttons = new QDialogButtonBox(this); mainLayout->addWidget(buttons); QPushButton *okButton = new QPushButton(this); okButton->setDefault(true); KGuiItem::assign(okButton, KStandardGuiItem::ok()); buttons->addButton(okButton, QDialogButtonBox::AcceptRole); connect(okButton, SIGNAL(clicked()), this, SLOT(apply())); QPushButton *cancelButton = new QPushButton(this); KGuiItem::assign(cancelButton, KStandardGuiItem::cancel()); buttons->addButton(okButton, QDialogButtonBox::RejectRole); connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); } KateCompletionConfig::~ KateCompletionConfig() { delete ui; } void KateCompletionConfig::readConfig(const KConfigGroup &config) { configStart(); // Sorting ui->sorting->setChecked(config.readEntry("Sorting Enabled", true)); ui->sortingAlphabetical->setChecked(config.readEntry("Sort Alphabetically", true)); ui->sortingCaseSensitive->setChecked(config.readEntry("Case Sensitive Sort", false)); ui->sortingInheritanceDepth->setChecked(config.readEntry("Sort by Inheritance Depth", true)); // Filtering ui->filtering->setChecked(config.readEntry("Filtering Enabled", false)); ui->filteringContextMatchOnly->setChecked(config.readEntry("Filter by Context Match Only", false)); ui->filteringHideAttributes->setChecked(config.readEntry("Hide Completions by Attribute", false)); int attributes = config.readEntry("Filter Attribute Mask", 0); for (int i = 0; i < ui->filteringAttributesList->count(); ++i) { QListWidgetItem *item = ui->filteringAttributesList->item(i); if (i == 0) { // Avoid a negative shift. 0xFFFFFFFF80000000 is 1 << -1. item->setCheckState((0xFFFFFFFF80000000 & attributes) ? Qt::Checked : Qt::Unchecked); } else { item->setCheckState(((1 << (i -1)) & attributes) ? Qt::Checked : Qt::Unchecked); } } ui->filteringMaximumInheritanceDepth->setValue(config.readEntry("Filter by Maximum Inheritance Depth", 0)); // Grouping ui->grouping->setChecked(config.readEntry("Grouping Enabled", true)); m_groupingScopeType->setCheckState(0, config.readEntry("Group by Scope Type", true) ? Qt::Checked : Qt::Unchecked); m_groupingScope->setCheckState(0, config.readEntry("Group by Scope", false) ? Qt::Checked : Qt::Unchecked); m_groupingAccessType->setCheckState(0, config.readEntry("Group by Access Type", true) ? Qt::Checked : Qt::Unchecked); m_groupingItemType->setCheckState(0, config.readEntry("Group by Item Type", false) ? Qt::Checked : Qt::Unchecked); ui->accessConst->setChecked(config.readEntry("Group by Const", false)); ui->accessStatic->setChecked(config.readEntry("Group by Static", false)); ui->accessSignalSlot->setChecked(config.readEntry("Group by Signals and Slots", false)); // Column merging ui->columnMerging->setChecked(config.readEntry("Column Merging Enabled", true)); for (int i = 0; i < ui->columnMergeTree->topLevelItemCount(); ++i) { QTreeWidgetItem *item = ui->columnMergeTree->topLevelItem(i); ///Initialize a standard column-merging: Merge Scope, Name, Arguments and Postfix item->setCheckState(1, config.readEntry(QStringLiteral("Column %1 Merge").arg(i), (i == CodeCompletionModel::Scope || i == CodeCompletionModel::Name || i == CodeCompletionModel::Arguments)) ? Qt::Checked : Qt::Unchecked); item->setCheckState(2, config.readEntry(QStringLiteral("Column %1 Show").arg(i), true) ? Qt::Checked : Qt::Unchecked); } applyInternal(); configEnd(); } void KateCompletionConfig::writeConfig(KConfigGroup &config) { // Sorting config.writeEntry("Sorting Enabled", ui->sorting->isChecked()); config.writeEntry("Sort Alphabetically", ui->sortingAlphabetical->isChecked()); config.writeEntry("Case Sensitive Sort", ui->sortingCaseSensitive->isChecked()); config.writeEntry("Sort by Inheritance Depth", ui->sortingInheritanceDepth->isChecked()); // Filtering config.writeEntry("Filtering Enabled", ui->filtering->isChecked()); config.writeEntry("Filter by Context Match Only", ui->filteringContextMatchOnly->isChecked()); config.writeEntry("Hide Completions by Attribute", ui->filteringHideAttributes->isChecked()); int attributes = 0; for (int i = 0; i < ui->filteringAttributesList->count(); ++i) { QListWidgetItem *item = ui->filteringAttributesList->item(i); if (item->checkState() == Qt::Checked) { if (i == 0) { // Avoid a negative shift. 0xFFFFFFFF80000000 is 1 << -1. attributes |= 0xFFFFFFFF80000000; } else { attributes |= 1 << (i - 1); } } } config.writeEntry("Filter Attribute Mask", attributes); config.writeEntry("Filter by Maximum Inheritance Depth", ui->filteringMaximumInheritanceDepth->value()); // Grouping config.writeEntry("Grouping Enabled", ui->grouping->isChecked()); config.writeEntry("Group by Scope Type", m_groupingScopeType->checkState(0) == Qt::Checked ? true : false); config.writeEntry("Group by Scope", m_groupingScope->checkState(0) == Qt::Checked ? true : false); config.writeEntry("Group by Access Type", m_groupingAccessType->checkState(0) == Qt::Checked ? true : false); config.writeEntry("Group by Item Type", m_groupingItemType->checkState(0) == Qt::Checked ? true : false); config.writeEntry("Group by Const", ui->accessConst->isChecked()); config.writeEntry("Group by Static", ui->accessStatic->isChecked()); config.writeEntry("Group by Signals and Slots", ui->accessSignalSlot->isChecked()); // Column merging config.writeEntry("Column Merging Enabled", ui->columnMerging->isChecked()); for (int i = 0; i < ui->columnMergeTree->topLevelItemCount(); ++i) { QTreeWidgetItem *item = ui->columnMergeTree->topLevelItem(i); config.writeEntry(QStringLiteral("Column %1 Merge").arg(i), item->checkState(1) == Qt::Checked ? true : false); config.writeEntry(QStringLiteral("Column %1 Show").arg(i), item->checkState(2) == Qt::Checked ? true : false); } config.sync(); } void KateCompletionConfig::updateConfig() { // Ah, nothing to do, I think...? } void KateCompletionConfig::moveColumnUp() { QTreeWidgetItem *item = ui->columnMergeTree->currentItem(); if (item) { int index = ui->columnMergeTree->indexOfTopLevelItem(item); if (index > 0) { ui->columnMergeTree->takeTopLevelItem(index); ui->columnMergeTree->insertTopLevelItem(index - 1, item); ui->columnMergeTree->setCurrentItem(item); } } } void KateCompletionConfig::moveColumnDown() { QTreeWidgetItem *item = ui->columnMergeTree->currentItem(); if (item) { int index = ui->columnMergeTree->indexOfTopLevelItem(item); if (index < ui->columnMergeTree->topLevelItemCount() - 1) { ui->columnMergeTree->takeTopLevelItem(index); ui->columnMergeTree->insertTopLevelItem(index + 1, item); ui->columnMergeTree->setCurrentItem(item); } } } void KateCompletionConfig::apply() { applyInternal(); KConfigGroup config(KTextEditor::EditorPrivate::config(), "Code Completion"); writeConfig(config); } void KateCompletionConfig::applyInternal() { // Sorting m_model->setSortingEnabled(ui->sorting->isChecked()); m_model->setSortingAlphabetical(ui->sortingAlphabetical->isChecked()); m_model->setSortingCaseSensitivity(ui->sortingCaseSensitive->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive); m_model->setSortingByInheritanceDepth(ui->sortingInheritanceDepth->isChecked()); // Filtering m_model->setFilteringEnabled(ui->filtering->isChecked()); m_model->setFilterContextMatchesOnly(ui->filteringContextMatchOnly->isChecked()); m_model->setFilterByAttribute(ui->filteringHideAttributes->isChecked()); CodeCompletionModel::CompletionProperties attributes = CodeCompletionModel::NoProperty; for (int i = 0; i < ui->filteringAttributesList->count(); ++i) { QListWidgetItem *item = ui->filteringAttributesList->item(i); if (item->checkState() == Qt::Checked) { attributes |= static_cast(item->type()); } } m_model->setFilterAttributes(attributes); m_model->setMaximumInheritanceDepth(ui->filteringMaximumInheritanceDepth->value()); // Grouping m_model->setGroupingEnabled(ui->grouping->isChecked()); KateCompletionModel::GroupingMethods groupingMethod = KateCompletionModel::GroupingMethods(); if (m_groupingScopeType->checkState(0) == Qt::Checked) { groupingMethod = KateCompletionModel::ScopeType; } if (m_groupingScope->checkState(0) == Qt::Checked) { groupingMethod |= KateCompletionModel::Scope; } if (m_groupingAccessType->checkState(0) == Qt::Checked) { groupingMethod |= KateCompletionModel::AccessType; } if (m_groupingItemType->checkState(0) == Qt::Checked) { groupingMethod |= KateCompletionModel::ItemType; } m_model->setGroupingMethod(groupingMethod); m_model->setAccessIncludeConst(ui->accessConst->isChecked()); m_model->setAccessIncludeStatic(ui->accessStatic->isChecked()); m_model->setAccessIncludeSignalSlot(ui->accessSignalSlot->isChecked()); // Column merging m_model->setColumnMergingEnabled(ui->columnMerging->isChecked()); QList< QList > mergedColumns; QList oneMerge; for (int i = 0; i < ui->columnMergeTree->topLevelItemCount(); ++i) { QTreeWidgetItem *item = ui->columnMergeTree->topLevelItem(i); if (item->type() != KTextEditor::CodeCompletionModel::Name && item->checkState(2) == Qt::Unchecked) { continue; } if (item->checkState(1) == Qt::Unchecked) { if (!oneMerge.isEmpty()) { mergedColumns.append(oneMerge); } oneMerge.clear(); } oneMerge.append(item->type()); } if (!oneMerge.isEmpty()) { mergedColumns.append(oneMerge); } m_model->setColumnMerges(mergedColumns); } void KateCompletionConfig::moveGroupingUp() { QTreeWidgetItem *item = ui->groupingMethods->currentItem(); if (item) { int index = ui->groupingMethods->indexOfTopLevelItem(item); if (index > 0) { ui->groupingMethods->takeTopLevelItem(index); ui->groupingMethods->insertTopLevelItem(index - 1, item); ui->groupingMethods->setCurrentItem(item); } } } void KateCompletionConfig::moveGroupingDown() { QTreeWidgetItem *item = ui->groupingMethods->currentItem(); if (item) { int index = ui->groupingMethods->indexOfTopLevelItem(item); if (index < ui->groupingMethods->topLevelItemCount() - 1) { ui->groupingMethods->takeTopLevelItem(index); ui->groupingMethods->insertTopLevelItem(index + 1, item); ui->groupingMethods->setCurrentItem(item); } } } void KateCompletionConfig::moveGroupingOrderUp() { QListWidgetItem *item = ui->sortGroupingOrder->currentItem(); int index = ui->sortGroupingOrder->currentRow(); if (index > 0) { ui->sortGroupingOrder->takeItem(index); ui->sortGroupingOrder->insertItem(index - 1, item); ui->sortGroupingOrder->setCurrentItem(item); } } void KateCompletionConfig::moveGroupingOrderDown() { QListWidgetItem *item = ui->sortGroupingOrder->currentItem(); int index = ui->sortGroupingOrder->currentRow(); if (index < ui->sortGroupingOrder->count() - 1) { ui->sortGroupingOrder->takeItem(index); ui->sortGroupingOrder->insertItem(index + 1, item); ui->sortGroupingOrder->setCurrentItem(item); } } diff --git a/src/dialogs/katedialogs.cpp b/src/dialogs/katedialogs.cpp index 5588b504..c5981520 100644 --- a/src/dialogs/katedialogs.cpp +++ b/src/dialogs/katedialogs.cpp @@ -1,1432 +1,1432 @@ /* This file is part of the KDE libraries Copyright (C) 2002, 2003 Anders Lund Copyright (C) 2003 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2006 Dominik Haumann Copyright (C) 2007 Mirko Stocker Copyright (C) 2009 Michel Ludwig Copyright (C) 2009 Erlend Hamberg Based on work of: Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //BEGIN Includes #include "katedialogs.h" #include #include #include "kateautoindent.h" #include "katebuffer.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "kateschema.h" #include "katemodeconfigpage.h" #include "kateview.h" #include "spellcheck/spellcheck.h" // auto generated ui files #include "ui_textareaappearanceconfigwidget.h" #include "ui_bordersappearanceconfigwidget.h" #include "ui_navigationconfigwidget.h" #include "ui_editconfigwidget.h" #include "ui_indentationconfigwidget.h" #include "ui_completionconfigtab.h" #include "ui_opensaveconfigwidget.h" #include "ui_opensaveconfigadvwidget.h" #include "ui_spellcheckconfigwidget.h" #include #include #include #include #include #include #include "katepartdebug.h" #include "kateabstractinputmodefactory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //END //BEGIN KateIndentConfigTab KateIndentConfigTab::KateIndentConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::IndentationConfigWidget(); ui->setupUi(newWidget); ui->cmbMode->addItems(KateAutoIndent::listModes()); // FIXME Give ui->label a more descriptive name, it's these "More..." info about tab key action ui->label->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); connect(ui->label, &QLabel::linkActivated, this, &KateIndentConfigTab::showWhatsThis); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->chkBackspaceUnindents); observeChanges(ui->chkIndentPaste); observeChanges(ui->chkKeepExtraSpaces); observeChanges(ui->cmbMode); observeChanges(ui->rbIndentMixed); observeChanges(ui->rbIndentWithSpaces); observeChanges(ui->rbIndentWithTabs); connect(ui->rbIndentWithTabs, &QAbstractButton::toggled, ui->sbIndentWidth, &QWidget::setDisabled); connect(ui->rbIndentWithTabs, &QAbstractButton::toggled, this, &KateIndentConfigTab::slotChanged); // FIXME See slot below observeChanges(ui->rbTabAdvances); observeChanges(ui->rbTabIndents); observeChanges(ui->rbTabSmart); observeChanges(ui->sbIndentWidth); observeChanges(ui->sbTabWidth); layout->addWidget(newWidget); setLayout(layout); } KateIndentConfigTab::~KateIndentConfigTab() { delete ui; } void KateIndentConfigTab::slotChanged() { // FIXME Make it working without this quirk // When the value is not copied it will silently set back to "Tabs & Spaces" if (ui->rbIndentWithTabs->isChecked()) { ui->sbIndentWidth->setValue(ui->sbTabWidth->value()); } } // NOTE Should we have more use of such info stuff, consider to make it part // of KateConfigPage and add a similar function like observeChanges(..) void KateIndentConfigTab::showWhatsThis(const QString &text) { QWhatsThis::showText(QCursor::pos(), text); } void KateIndentConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateDocumentConfig::global()->configStart(); KateDocumentConfig::global()->setBackspaceIndents(ui->chkBackspaceUnindents->isChecked()); KateDocumentConfig::global()->setIndentPastedText(ui->chkIndentPaste->isChecked()); KateDocumentConfig::global()->setIndentationMode(KateAutoIndent::modeName(ui->cmbMode->currentIndex())); KateDocumentConfig::global()->setIndentationWidth(ui->sbIndentWidth->value()); KateDocumentConfig::global()->setKeepExtraSpaces(ui->chkKeepExtraSpaces->isChecked()); KateDocumentConfig::global()->setReplaceTabsDyn(ui->rbIndentWithSpaces->isChecked()); KateDocumentConfig::global()->setTabWidth(ui->sbTabWidth->value()); if (ui->rbTabAdvances->isChecked()) { KateDocumentConfig::global()->setTabHandling(KateDocumentConfig::tabInsertsTab); } else if (ui->rbTabIndents->isChecked()) { KateDocumentConfig::global()->setTabHandling(KateDocumentConfig::tabIndents); } else { KateDocumentConfig::global()->setTabHandling(KateDocumentConfig::tabSmart); } KateDocumentConfig::global()->configEnd(); } void KateIndentConfigTab::reload() { ui->chkBackspaceUnindents->setChecked(KateDocumentConfig::global()->backspaceIndents()); ui->chkIndentPaste->setChecked(KateDocumentConfig::global()->indentPastedText()); ui->chkKeepExtraSpaces->setChecked(KateDocumentConfig::global()->keepExtraSpaces()); ui->sbIndentWidth->setSuffix(ki18np(" character", " characters")); ui->sbIndentWidth->setValue(KateDocumentConfig::global()->indentationWidth()); ui->sbTabWidth->setSuffix(ki18np(" character", " characters")); ui->sbTabWidth->setValue(KateDocumentConfig::global()->tabWidth()); ui->rbTabAdvances->setChecked(KateDocumentConfig::global()->tabHandling() == KateDocumentConfig::tabInsertsTab); ui->rbTabIndents->setChecked(KateDocumentConfig::global()->tabHandling() == KateDocumentConfig::tabIndents); ui->rbTabSmart->setChecked(KateDocumentConfig::global()->tabHandling() == KateDocumentConfig::tabSmart); ui->cmbMode->setCurrentIndex(KateAutoIndent::modeNumber(KateDocumentConfig::global()->indentationMode())); if (KateDocumentConfig::global()->replaceTabsDyn()) { ui->rbIndentWithSpaces->setChecked(true); } else { if (KateDocumentConfig::global()->indentationWidth() == KateDocumentConfig::global()->tabWidth()) { ui->rbIndentWithTabs->setChecked(true); } else { ui->rbIndentMixed->setChecked(true); } } ui->sbIndentWidth->setEnabled(!ui->rbIndentWithTabs->isChecked()); } QString KateIndentConfigTab::name() const { return i18n("Indentation"); } //END KateIndentConfigTab //BEGIN KateCompletionConfigTab KateCompletionConfigTab::KateCompletionConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::CompletionConfigTab(); ui->setupUi(newWidget); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->chkAutoCompletionEnabled); observeChanges(ui->gbKeywordCompletion); observeChanges(ui->gbWordCompletion); observeChanges(ui->minimalWordLength); observeChanges(ui->removeTail); layout->addWidget(newWidget); setLayout(layout); } KateCompletionConfigTab::~KateCompletionConfigTab() { delete ui; } void KateCompletionConfigTab::showWhatsThis(const QString &text) // NOTE Not used atm, remove? See also KateIndentConfigTab::showWhatsThis { QWhatsThis::showText(QCursor::pos(), text); } void KateCompletionConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateViewConfig::global()->setValue(KateViewConfig::AutomaticCompletionInvocation, ui->chkAutoCompletionEnabled->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::KeywordCompletion, ui->gbKeywordCompletion->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::WordCompletion, ui->gbWordCompletion->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::WordCompletionMinimalWordLength, ui->minimalWordLength->value()); KateViewConfig::global()->setValue(KateViewConfig::WordCompletionRemoveTail, ui->removeTail->isChecked()); KateViewConfig::global()->configEnd(); } void KateCompletionConfigTab::reload() { ui->chkAutoCompletionEnabled->setChecked(KateViewConfig::global()->automaticCompletionInvocation()); ui->gbKeywordCompletion->setChecked(KateViewConfig::global()->keywordCompletion()); ui->gbWordCompletion->setChecked(KateViewConfig::global()->wordCompletion()); ui->minimalWordLength->setValue(KateViewConfig::global()->wordCompletionMinimalWordLength()); ui->removeTail->setChecked(KateViewConfig::global()->wordCompletionRemoveTail()); } QString KateCompletionConfigTab::name() const { return i18n("Auto Completion"); } //END KateCompletionConfigTab //BEGIN KateSpellCheckConfigTab KateSpellCheckConfigTab::KateSpellCheckConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us have more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::SpellCheckConfigWidget(); ui->setupUi(newWidget); // "What's This?" help can be found in the ui file reload(); m_sonnetConfigWidget = new Sonnet::ConfigWidget(this); connect(m_sonnetConfigWidget, &Sonnet::ConfigWidget::configChanged, this, &KateSpellCheckConfigTab::slotChanged); layout->addWidget(m_sonnetConfigWidget); layout->addWidget(newWidget); setLayout(layout); } KateSpellCheckConfigTab::~KateSpellCheckConfigTab() { delete ui; } void KateSpellCheckConfigTab::showWhatsThis(const QString &text) // NOTE Not used atm, remove? See also KateIndentConfigTab::showWhatsThis { QWhatsThis::showText(QCursor::pos(), text); } void KateSpellCheckConfigTab::apply() { if (!hasChanged()) { // nothing changed, no need to apply stuff return; } m_changed = false; // WARNING: this is slightly hackish, but it's currently the only way to // do it, see also the KTextEdit class KateDocumentConfig::global()->configStart(); m_sonnetConfigWidget->save(); QSettings settings(QStringLiteral("KDE"), QStringLiteral("Sonnet")); KateDocumentConfig::global()->setOnTheFlySpellCheck(settings.value(QStringLiteral("checkerEnabledByDefault"), false).toBool()); KateDocumentConfig::global()->configEnd(); foreach (KTextEditor::DocumentPrivate *doc, KTextEditor::EditorPrivate::self()->kateDocuments()) { doc->refreshOnTheFlyCheck(); } } void KateSpellCheckConfigTab::reload() { // does nothing } QString KateSpellCheckConfigTab::name() const { return i18n("Spellcheck"); } //END KateSpellCheckConfigTab //BEGIN KateNavigationConfigTab KateNavigationConfigTab::KateNavigationConfigTab(QWidget *parent) : KateConfigPage(parent) { // This will let us having more separation between this page and // the QTabWidget edge (ereslibre) QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::NavigationConfigWidget(); ui->setupUi(newWidget); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->cbTextSelectionMode); observeChanges(ui->chkBackspaceRemoveComposed); observeChanges(ui->chkPagingMovesCursor); observeChanges(ui->chkScrollPastEnd); observeChanges(ui->chkSmartHome); observeChanges(ui->sbAutoCenterCursor); layout->addWidget(newWidget); setLayout(layout); } KateNavigationConfigTab::~KateNavigationConfigTab() { delete ui; } void KateNavigationConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); KateDocumentConfig::global()->setPageUpDownMovesCursor(ui->chkPagingMovesCursor->isChecked()); KateDocumentConfig::global()->setSmartHome(ui->chkSmartHome->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::AutoCenterLines, ui->sbAutoCenterCursor->value()); KateViewConfig::global()->setValue(KateViewConfig::BackspaceRemoveComposedCharacters, ui->chkBackspaceRemoveComposed->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::PersistentSelection, ui->cbTextSelectionMode->currentIndex() == 1); KateViewConfig::global()->setValue(KateViewConfig::ScrollPastEnd, ui->chkScrollPastEnd->isChecked()); KateDocumentConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); } void KateNavigationConfigTab::reload() { ui->cbTextSelectionMode->setCurrentIndex(KateViewConfig::global()->persistentSelection() ? 1 : 0); ui->chkBackspaceRemoveComposed->setChecked(KateViewConfig::global()->backspaceRemoveComposed()); ui->chkPagingMovesCursor->setChecked(KateDocumentConfig::global()->pageUpDownMovesCursor()); ui->chkScrollPastEnd->setChecked(KateViewConfig::global()->scrollPastEnd()); ui->chkSmartHome->setChecked(KateDocumentConfig::global()->smartHome()); ui->sbAutoCenterCursor->setValue(KateViewConfig::global()->autoCenterLines()); } QString KateNavigationConfigTab::name() const { return i18n("Text Navigation"); } //END KateNavigationConfigTab //BEGIN KateEditGeneralConfigTab KateEditGeneralConfigTab::KateEditGeneralConfigTab(QWidget *parent) : KateConfigPage(parent) { QVBoxLayout *layout = new QVBoxLayout; QWidget *newWidget = new QWidget(this); ui = new Ui::EditConfigWidget(); ui->setupUi(newWidget); QList inputModes = KTextEditor::EditorPrivate::self()->inputModeFactories(); Q_FOREACH(KateAbstractInputModeFactory *fact, inputModes) { ui->cmbInputMode->addItem(fact->name(), static_cast(fact->inputMode())); } // "What's This?" Help is in the ui-files reload(); observeChanges(ui->chkAutoBrackets); observeChanges(ui->chkMousePasteAtCursorPosition); observeChanges(ui->chkShowStaticWordWrapMarker); observeChanges(ui->chkTextDragAndDrop); observeChanges(ui->chkSmartCopyCut); observeChanges(ui->chkStaticWordWrap); observeChanges(ui->cmbEncloseSelection); connect(ui->cmbEncloseSelection->lineEdit(), &QLineEdit::editingFinished, [=] { const int index = ui->cmbEncloseSelection->currentIndex(); const QString text = ui->cmbEncloseSelection->currentText(); // Text removed? Remove item, but don't remove default data! if (index >= UserData && text.isEmpty()) { ui->cmbEncloseSelection->removeItem(index); slotChanged(); // Not already there? Add new item! For whatever reason it isn't done automatically } else if (ui->cmbEncloseSelection->findText(text) < 0) { ui->cmbEncloseSelection->addItem(text); slotChanged(); } ui->cmbEncloseSelection->setCurrentIndex(ui->cmbEncloseSelection->findText(text)); }); observeChanges(ui->cmbInputMode); observeChanges(ui->sbWordWrap); layout->addWidget(newWidget); setLayout(layout); } KateEditGeneralConfigTab::~KateEditGeneralConfigTab() { delete ui; } void KateEditGeneralConfigTab::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); KateDocumentConfig::global()->setWordWrap(ui->chkStaticWordWrap->isChecked()); KateDocumentConfig::global()->setWordWrapAt(ui->sbWordWrap->value()); KateRendererConfig::global()->setWordWrapMarker(ui->chkShowStaticWordWrapMarker->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::AutoBrackets, ui->chkAutoBrackets->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::CharsToEncloseSelection, ui->cmbEncloseSelection->currentText()); QStringList userLetters; for (int i = UserData; i < ui->cmbEncloseSelection->count(); ++i) { userLetters.append(ui->cmbEncloseSelection->itemText(i)); } KateViewConfig::global()->setValue(KateViewConfig::UserSetsOfCharsToEncloseSelection, userLetters); KateViewConfig::global()->setValue(KateViewConfig::InputMode, ui->cmbInputMode->currentData().toInt()); KateViewConfig::global()->setValue(KateViewConfig::MousePasteAtCursorPosition, ui->chkMousePasteAtCursorPosition->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::TextDragAndDrop, ui->chkTextDragAndDrop->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::SmartCopyCut, ui->chkSmartCopyCut->isChecked()); KateDocumentConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); } void KateEditGeneralConfigTab::reload() { ui->chkAutoBrackets->setChecked(KateViewConfig::global()->autoBrackets()); ui->chkMousePasteAtCursorPosition->setChecked(KateViewConfig::global()->mousePasteAtCursorPosition()); ui->chkShowStaticWordWrapMarker->setChecked(KateRendererConfig::global()->wordWrapMarker()); ui->chkTextDragAndDrop->setChecked(KateViewConfig::global()->textDragAndDrop()); ui->chkSmartCopyCut->setChecked(KateViewConfig::global()->smartCopyCut()); ui->chkStaticWordWrap->setChecked(KateDocumentConfig::global()->wordWrap()); ui->sbWordWrap->setSuffix(ki18ncp("Wrap words at (value is at 20 or larger)", " character", " characters")); ui->sbWordWrap->setValue(KateDocumentConfig::global()->wordWrapAt()); ui->cmbEncloseSelection->clear(); ui->cmbEncloseSelection->lineEdit()->setClearButtonEnabled(true); ui->cmbEncloseSelection->lineEdit()->setPlaceholderText(QStringLiteral("Feature is not active")); ui->cmbEncloseSelection->addItem(QString(), None); ui->cmbEncloseSelection->setItemData(0, i18n("Disable Feature"), Qt::ToolTipRole); ui->cmbEncloseSelection->addItem(QStringLiteral("`*_~"), MarkDown); ui->cmbEncloseSelection->setItemData(1, i18n("May be handy with Markdown"), Qt::ToolTipRole); ui->cmbEncloseSelection->addItem(QStringLiteral("<>(){}[]"), MirrorChar); ui->cmbEncloseSelection->setItemData(2, i18n("Mirror characters, similar but not exactly like auto brackets"), Qt::ToolTipRole); ui->cmbEncloseSelection->addItem(QStringLiteral("´`_.:|#@~*!?$%/=,;-+^°§&"), NonLetters); ui->cmbEncloseSelection->setItemData(3, i18n("Non letter character"), Qt::ToolTipRole); const QStringList userLetters = KateViewConfig::global()->value(KateViewConfig::UserSetsOfCharsToEncloseSelection).toStringList(); for (int i = 0; i < userLetters.size(); ++i) { ui->cmbEncloseSelection->addItem(userLetters.at(i), UserData + i); } ui->cmbEncloseSelection->setCurrentIndex(ui->cmbEncloseSelection->findText(KateViewConfig::global()->charsToEncloseSelection())); const int id = static_cast(KateViewConfig::global()->inputMode()); ui->cmbInputMode->setCurrentIndex(ui->cmbInputMode->findData(id)); } QString KateEditGeneralConfigTab::name() const { return i18n("General"); } //END KateEditGeneralConfigTab //BEGIN KateEditConfigTab KateEditConfigTab::KateEditConfigTab(QWidget *parent) : KateConfigPage(parent) , editConfigTab(new KateEditGeneralConfigTab(this)) , navigationConfigTab(new KateNavigationConfigTab(this)) , indentConfigTab(new KateIndentConfigTab(this)) , completionConfigTab(new KateCompletionConfigTab(this)) , spellCheckConfigTab(new KateSpellCheckConfigTab(this)) { QVBoxLayout *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); QTabWidget *tabWidget = new QTabWidget(this); // add all tabs tabWidget->insertTab(0, editConfigTab, editConfigTab->name()); tabWidget->insertTab(1, navigationConfigTab, navigationConfigTab->name()); tabWidget->insertTab(2, indentConfigTab, indentConfigTab->name()); tabWidget->insertTab(3, completionConfigTab, completionConfigTab->name()); tabWidget->insertTab(4, spellCheckConfigTab, spellCheckConfigTab->name()); observeChanges(editConfigTab); observeChanges(navigationConfigTab); observeChanges(indentConfigTab); observeChanges(completionConfigTab); observeChanges(spellCheckConfigTab); int i = tabWidget->count(); Q_FOREACH(KateAbstractInputModeFactory *factory, KTextEditor::EditorPrivate::self()->inputModeFactories()) { KateConfigPage *tab = factory->createConfigPage(this); if (tab) { m_inputModeConfigTabs << tab; tabWidget->insertTab(i, tab, tab->name()); observeChanges(tab); i++; } } layout->addWidget(tabWidget); setLayout(layout); } KateEditConfigTab::~KateEditConfigTab() { qDeleteAll(m_inputModeConfigTabs); } void KateEditConfigTab::apply() { // try to update the rest of tabs editConfigTab->apply(); navigationConfigTab->apply(); indentConfigTab->apply(); completionConfigTab->apply(); spellCheckConfigTab->apply(); Q_FOREACH(KateConfigPage *tab, m_inputModeConfigTabs) { tab->apply(); } } void KateEditConfigTab::reload() { editConfigTab->reload(); navigationConfigTab->reload(); indentConfigTab->reload(); completionConfigTab->reload(); spellCheckConfigTab->reload(); Q_FOREACH(KateConfigPage *tab, m_inputModeConfigTabs) { tab->reload(); } } void KateEditConfigTab::reset() { editConfigTab->reset(); navigationConfigTab->reset(); indentConfigTab->reset(); completionConfigTab->reset(); spellCheckConfigTab->reset(); Q_FOREACH(KateConfigPage *tab, m_inputModeConfigTabs) { tab->reset(); } } void KateEditConfigTab::defaults() { editConfigTab->defaults(); navigationConfigTab->defaults(); indentConfigTab->defaults(); completionConfigTab->defaults(); spellCheckConfigTab->defaults(); Q_FOREACH(KateConfigPage *tab, m_inputModeConfigTabs) { tab->defaults(); } } QString KateEditConfigTab::name() const { return i18n("Editing"); } QString KateEditConfigTab::fullName() const { return i18n("Editing Options"); } QIcon KateEditConfigTab::icon() const { return QIcon::fromTheme(QStringLiteral("accessories-text-editor")); } //END KateEditConfigTab //BEGIN KateViewDefaultsConfig KateViewDefaultsConfig::KateViewDefaultsConfig(QWidget *parent) : KateConfigPage(parent) , textareaUi(new Ui::TextareaAppearanceConfigWidget()) , bordersUi(new Ui::BordersAppearanceConfigWidget()) { QLayout *layout = new QVBoxLayout(this); QTabWidget *tabWidget = new QTabWidget(this); layout->addWidget(tabWidget); layout->setContentsMargins(0, 0, 0, 0); QWidget *textareaTab = new QWidget(tabWidget); textareaUi->setupUi(textareaTab); tabWidget->addTab(textareaTab, i18n("General")); QWidget *bordersTab = new QWidget(tabWidget); bordersUi->setupUi(bordersTab); tabWidget->addTab(bordersTab, i18n("Borders")); textareaUi->cmbDynamicWordWrapIndicator->addItem(i18n("Off")); textareaUi->cmbDynamicWordWrapIndicator->addItem(i18n("Follow Line Numbers")); textareaUi->cmbDynamicWordWrapIndicator->addItem(i18n("Always On")); // "What's This?" help is in the ui-file reload(); observeChanges(textareaUi->chkAnimateBracketMatching); observeChanges(textareaUi->chkDynWrapAtStaticMarker); observeChanges(textareaUi->chkFoldFirstLine); observeChanges(textareaUi->chkShowIndentationLines); observeChanges(textareaUi->chkShowLineCount); observeChanges(textareaUi->chkShowTabs); observeChanges(textareaUi->chkShowWholeBracketExpression); observeChanges(textareaUi->chkShowWordCount); observeChanges(textareaUi->cmbDynamicWordWrapIndicator); observeChanges(textareaUi->gbWordWrap); observeChanges(textareaUi->sbDynamicWordWrapDepth); observeChanges(textareaUi->sliSetMarkerSize); observeChanges(textareaUi->spacesComboBox); observeChanges(bordersUi->chkIconBorder); observeChanges(bordersUi->chkLineNumbers); observeChanges(bordersUi->chkScrollbarMarks); observeChanges(bordersUi->chkScrollbarMiniMap); observeChanges(bordersUi->chkScrollbarMiniMapAll); bordersUi->chkScrollbarMiniMapAll->hide(); // this is temporary until the feature is done observeChanges(bordersUi->chkScrollbarPreview); observeChanges(bordersUi->chkShowFoldingMarkers); observeChanges(bordersUi->chkShowFoldingPreview); observeChanges(bordersUi->chkShowLineModification); observeChanges(bordersUi->cmbShowScrollbars); observeChanges(bordersUi->rbSortBookmarksByCreation); observeChanges(bordersUi->rbSortBookmarksByPosition); observeChanges(bordersUi->spBoxMiniMapWidth); } KateViewDefaultsConfig::~KateViewDefaultsConfig() { delete bordersUi; delete textareaUi; } void KateViewDefaultsConfig::apply() { // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateViewConfig::global()->configStart(); KateRendererConfig::global()->configStart(); KateDocumentConfig::global()->setMarkerSize(textareaUi->sliSetMarkerSize->value()); KateDocumentConfig::global()->setShowSpaces(KateDocumentConfig::WhitespaceRendering(textareaUi->spacesComboBox->currentIndex())); KateDocumentConfig::global()->setShowTabs(textareaUi->chkShowTabs->isChecked()); KateRendererConfig::global()->setAnimateBracketMatching(textareaUi->chkAnimateBracketMatching->isChecked()); KateRendererConfig::global()->setShowIndentationLines(textareaUi->chkShowIndentationLines->isChecked()); KateRendererConfig::global()->setShowWholeBracketExpression(textareaUi->chkShowWholeBracketExpression->isChecked()); KateViewConfig::global()->setDynWordWrap(textareaUi->gbWordWrap->isChecked()); KateViewConfig::global()->setShowWordCount(textareaUi->chkShowWordCount->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::BookmarkSorting, bordersUi->rbSortBookmarksByPosition->isChecked() ? 0 : 1); KateViewConfig::global()->setValue(KateViewConfig::DynWordWrapAlignIndent, textareaUi->sbDynamicWordWrapDepth->value()); KateViewConfig::global()->setValue(KateViewConfig::DynWordWrapIndicators, textareaUi->cmbDynamicWordWrapIndicator->currentIndex()); KateViewConfig::global()->setValue(KateViewConfig::DynWrapAtStaticMarker, textareaUi->chkDynWrapAtStaticMarker->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::FoldFirstLine, textareaUi->chkFoldFirstLine->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ScrollBarMiniMapWidth, bordersUi->spBoxMiniMapWidth->value()); KateViewConfig::global()->setValue(KateViewConfig::ShowFoldingBar, bordersUi->chkShowFoldingMarkers->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowFoldingPreview, bordersUi->chkShowFoldingPreview->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowIconBar, bordersUi->chkIconBorder->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowLineCount, textareaUi->chkShowLineCount->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowLineModification, bordersUi->chkShowLineModification->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowLineNumbers, bordersUi->chkLineNumbers->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarMarks, bordersUi->chkScrollbarMarks->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarMiniMap, bordersUi->chkScrollbarMiniMap->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarMiniMapAll, bordersUi->chkScrollbarMiniMapAll->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollBarPreview, bordersUi->chkScrollbarPreview->isChecked()); KateViewConfig::global()->setValue(KateViewConfig::ShowScrollbars, bordersUi->cmbShowScrollbars->currentIndex()); KateRendererConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); } void KateViewDefaultsConfig::reload() { bordersUi->chkIconBorder->setChecked(KateViewConfig::global()->iconBar()); bordersUi->chkLineNumbers->setChecked(KateViewConfig::global()->lineNumbers()); bordersUi->chkScrollbarMarks->setChecked(KateViewConfig::global()->scrollBarMarks()); bordersUi->chkScrollbarMiniMap->setChecked(KateViewConfig::global()->scrollBarMiniMap()); bordersUi->chkScrollbarMiniMapAll->setChecked(KateViewConfig::global()->scrollBarMiniMapAll()); bordersUi->chkScrollbarPreview->setChecked(KateViewConfig::global()->scrollBarPreview()); bordersUi->chkShowFoldingMarkers->setChecked(KateViewConfig::global()->foldingBar()); bordersUi->chkShowFoldingPreview->setChecked(KateViewConfig::global()->foldingPreview()); bordersUi->chkShowLineModification->setChecked(KateViewConfig::global()->lineModification()); bordersUi->cmbShowScrollbars->setCurrentIndex(KateViewConfig::global()->showScrollbars()); bordersUi->rbSortBookmarksByCreation->setChecked(KateViewConfig::global()->bookmarkSort() == 1); bordersUi->rbSortBookmarksByPosition->setChecked(KateViewConfig::global()->bookmarkSort() == 0); bordersUi->spBoxMiniMapWidth->setValue(KateViewConfig::global()->scrollBarMiniMapWidth()); textareaUi->chkAnimateBracketMatching->setChecked(KateRendererConfig::global()->animateBracketMatching()); textareaUi->chkDynWrapAtStaticMarker->setChecked(KateViewConfig::global()->dynWrapAtStaticMarker()); textareaUi->chkFoldFirstLine->setChecked(KateViewConfig::global()->foldFirstLine()); textareaUi->chkShowIndentationLines->setChecked(KateRendererConfig::global()->showIndentationLines()); textareaUi->chkShowLineCount->setChecked(KateViewConfig::global()->showLineCount()); textareaUi->chkShowTabs->setChecked(KateDocumentConfig::global()->showTabs()); textareaUi->chkShowWholeBracketExpression->setChecked(KateRendererConfig::global()->showWholeBracketExpression()); textareaUi->chkShowWordCount->setChecked(KateViewConfig::global()->showWordCount()); textareaUi->cmbDynamicWordWrapIndicator->setCurrentIndex(KateViewConfig::global()->dynWordWrapIndicators()); textareaUi->gbWordWrap->setChecked(KateViewConfig::global()->dynWordWrap()); textareaUi->sbDynamicWordWrapDepth->setValue(KateViewConfig::global()->dynWordWrapAlignIndent()); textareaUi->sliSetMarkerSize->setValue(KateDocumentConfig::global()->markerSize()); textareaUi->spacesComboBox->setCurrentIndex(KateDocumentConfig::global()->showSpaces()); } void KateViewDefaultsConfig::reset() { ; } void KateViewDefaultsConfig::defaults() { ; } QString KateViewDefaultsConfig::name() const { return i18n("Appearance"); } QString KateViewDefaultsConfig::fullName() const { return i18n("Appearance"); } QIcon KateViewDefaultsConfig::icon() const { return QIcon::fromTheme(QStringLiteral("preferences-desktop-theme")); } //END KateViewDefaultsConfig //BEGIN KateSaveConfigTab KateSaveConfigTab::KateSaveConfigTab(QWidget *parent) : KateConfigPage(parent) , modeConfigPage(new ModeConfigPage(this)) { // FIXME: Is really needed to move all this code below to another class, // since it is another tab itself on the config dialog. This means we should // initialize, add and work with as we do with modeConfigPage (ereslibre) QVBoxLayout *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); QTabWidget *tabWidget = new QTabWidget(this); QWidget *tmpWidget = new QWidget(tabWidget); QVBoxLayout *internalLayout = new QVBoxLayout; QWidget *newWidget = new QWidget(tabWidget); ui = new Ui::OpenSaveConfigWidget(); ui->setupUi(newWidget); QWidget *tmpWidget2 = new QWidget(tabWidget); QVBoxLayout *internalLayout2 = new QVBoxLayout; QWidget *newWidget2 = new QWidget(tabWidget); uiadv = new Ui::OpenSaveConfigAdvWidget(); uiadv->setupUi(newWidget2); // "What's This?" help can be found in the ui file reload(); observeChanges(ui->cbRemoveTrailingSpaces); observeChanges(ui->chkDetectEOL); observeChanges(ui->chkEnableBOM); observeChanges(ui->chkNewLineAtEof); observeChanges(ui->cmbEOL); observeChanges(ui->cmbEncoding); observeChanges(ui->cmbEncodingDetection); observeChanges(ui->cmbEncodingFallback); observeChanges(ui->lineLengthLimit); observeChanges(uiadv->chkBackupLocalFiles); observeChanges(uiadv->chkBackupRemoteFiles); observeChanges(uiadv->cmbSwapFileMode); connect(uiadv->cmbSwapFileMode, QOverload::of(&QComboBox::currentIndexChanged), this, &KateSaveConfigTab::swapFileModeChanged); observeChanges(uiadv->edtBackupPrefix); observeChanges(uiadv->edtBackupSuffix); observeChanges(uiadv->kurlSwapDirectory); observeChanges(uiadv->spbSwapFileSync); internalLayout->addWidget(newWidget); tmpWidget->setLayout(internalLayout); internalLayout2->addWidget(newWidget2); tmpWidget2->setLayout(internalLayout2); // add all tabs tabWidget->insertTab(0, tmpWidget, i18n("General")); tabWidget->insertTab(1, tmpWidget2, i18n("Advanced")); tabWidget->insertTab(2, modeConfigPage, modeConfigPage->name()); observeChanges(modeConfigPage); layout->addWidget(tabWidget); setLayout(layout); } KateSaveConfigTab::~KateSaveConfigTab() { delete ui; } void KateSaveConfigTab::swapFileModeChanged(int idx) { const KateDocumentConfig::SwapFileMode mode = static_cast(idx); switch (mode) { case KateDocumentConfig::DisableSwapFile: uiadv->lblSwapDirectory->setEnabled(false); uiadv->kurlSwapDirectory->setEnabled(false); uiadv->lblSwapFileSync->setEnabled(false); uiadv->spbSwapFileSync->setEnabled(false); break; case KateDocumentConfig::EnableSwapFile: uiadv->lblSwapDirectory->setEnabled(false); uiadv->kurlSwapDirectory->setEnabled(false); uiadv->lblSwapFileSync->setEnabled(true); uiadv->spbSwapFileSync->setEnabled(true); break; case KateDocumentConfig::SwapFilePresetDirectory: uiadv->lblSwapDirectory->setEnabled(true); uiadv->kurlSwapDirectory->setEnabled(true); uiadv->lblSwapFileSync->setEnabled(true); uiadv->spbSwapFileSync->setEnabled(true); break; } } void KateSaveConfigTab::apply() { modeConfigPage->apply(); // nothing changed, no need to apply stuff if (!hasChanged()) { return; } m_changed = false; KateGlobalConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); if (uiadv->edtBackupSuffix->text().isEmpty() && uiadv->edtBackupPrefix->text().isEmpty()) { KMessageBox::information( this, i18n("You did not provide a backup suffix or prefix. Using default suffix: '~'"), i18n("No Backup Suffix or Prefix") ); uiadv->edtBackupSuffix->setText(QStringLiteral("~")); } KateDocumentConfig::global()->setBackupOnSaveLocal(uiadv->chkBackupLocalFiles->isChecked()); KateDocumentConfig::global()->setBackupOnSaveRemote(uiadv->chkBackupRemoteFiles->isChecked()); KateDocumentConfig::global()->setBackupPrefix(uiadv->edtBackupPrefix->text()); KateDocumentConfig::global()->setBackupSuffix(uiadv->edtBackupSuffix->text()); KateDocumentConfig::global()->setSwapFileMode(uiadv->cmbSwapFileMode->currentIndex()); KateDocumentConfig::global()->setSwapDirectory(uiadv->kurlSwapDirectory->url().toLocalFile()); KateDocumentConfig::global()->setSwapSyncInterval(uiadv->spbSwapFileSync->value()); KateDocumentConfig::global()->setRemoveSpaces(ui->cbRemoveTrailingSpaces->currentIndex()); KateDocumentConfig::global()->setNewLineAtEof(ui->chkNewLineAtEof->isChecked()); // set both standard and fallback encoding KateDocumentConfig::global()->setEncoding(KCharsets::charsets()->encodingForName(ui->cmbEncoding->currentText())); KateGlobalConfig::global()->setProberType((KEncodingProber::ProberType)ui->cmbEncodingDetection->currentIndex()); KateGlobalConfig::global()->setFallbackEncoding(KCharsets::charsets()->encodingForName(ui->cmbEncodingFallback->currentText())); KateDocumentConfig::global()->setEol(ui->cmbEOL->currentIndex()); KateDocumentConfig::global()->setAllowEolDetection(ui->chkDetectEOL->isChecked()); KateDocumentConfig::global()->setBom(ui->chkEnableBOM->isChecked()); KateDocumentConfig::global()->setLineLengthLimit(ui->lineLengthLimit->value()); KateDocumentConfig::global()->configEnd(); KateGlobalConfig::global()->configEnd(); } void KateSaveConfigTab::reload() { modeConfigPage->reload(); // encodings ui->cmbEncoding->clear(); ui->cmbEncodingFallback->clear(); QStringList encodings(KCharsets::charsets()->descriptiveEncodingNames()); int insert = 0; for (int i = 0; i < encodings.count(); i++) { bool found = false; QTextCodec *codecForEnc = KCharsets::charsets()->codecForName(KCharsets::charsets()->encodingForName(encodings[i]), found); if (found) { ui->cmbEncoding->addItem(encodings[i]); ui->cmbEncodingFallback->addItem(encodings[i]); if (codecForEnc == KateDocumentConfig::global()->codec()) { ui->cmbEncoding->setCurrentIndex(insert); } if (codecForEnc == KateGlobalConfig::global()->fallbackCodec()) { // adjust index for fallback config, has no default! ui->cmbEncodingFallback->setCurrentIndex(insert); } insert++; } } // encoding detection ui->cmbEncodingDetection->clear(); bool found = false; for (int i = 0; !KEncodingProber::nameForProberType((KEncodingProber::ProberType) i).isEmpty(); ++i) { ui->cmbEncodingDetection->addItem(KEncodingProber::nameForProberType((KEncodingProber::ProberType) i)); if (i == KateGlobalConfig::global()->proberType()) { ui->cmbEncodingDetection->setCurrentIndex(ui->cmbEncodingDetection->count() - 1); found = true; } } if (!found) { ui->cmbEncodingDetection->setCurrentIndex(KEncodingProber::Universal); } // eol ui->cmbEOL->setCurrentIndex(KateDocumentConfig::global()->eol()); ui->chkDetectEOL->setChecked(KateDocumentConfig::global()->allowEolDetection()); ui->chkEnableBOM->setChecked(KateDocumentConfig::global()->bom()); ui->lineLengthLimit->setValue(KateDocumentConfig::global()->lineLengthLimit()); ui->cbRemoveTrailingSpaces->setCurrentIndex(KateDocumentConfig::global()->removeSpaces()); ui->chkNewLineAtEof->setChecked(KateDocumentConfig::global()->newLineAtEof()); // other stuff uiadv->chkBackupLocalFiles->setChecked(KateDocumentConfig::global()->backupOnSaveLocal()); uiadv->chkBackupRemoteFiles->setChecked(KateDocumentConfig::global()->backupOnSaveRemote()); uiadv->edtBackupPrefix->setText(KateDocumentConfig::global()->backupPrefix()); uiadv->edtBackupSuffix->setText(KateDocumentConfig::global()->backupSuffix()); uiadv->cmbSwapFileMode->setCurrentIndex(KateDocumentConfig::global()->swapFileMode()); uiadv->kurlSwapDirectory->setUrl(QUrl::fromLocalFile(KateDocumentConfig::global()->swapDirectory())); uiadv->spbSwapFileSync->setValue(KateDocumentConfig::global()->swapSyncInterval()); swapFileModeChanged(KateDocumentConfig::global()->swapFileMode()); } void KateSaveConfigTab::reset() { modeConfigPage->reset(); } void KateSaveConfigTab::defaults() { modeConfigPage->defaults(); ui->cbRemoveTrailingSpaces->setCurrentIndex(0); uiadv->chkBackupLocalFiles->setChecked(true); uiadv->chkBackupRemoteFiles->setChecked(false); uiadv->edtBackupPrefix->setText(QString()); uiadv->edtBackupSuffix->setText(QStringLiteral("~")); uiadv->cmbSwapFileMode->setCurrentIndex(1); uiadv->kurlSwapDirectory->setDisabled(true); uiadv->lblSwapDirectory->setDisabled(true); uiadv->spbSwapFileSync->setValue(15); } QString KateSaveConfigTab::name() const { return i18n("Open/Save"); } QString KateSaveConfigTab::fullName() const { return i18n("File Opening & Saving"); } QIcon KateSaveConfigTab::icon() const { return QIcon::fromTheme(QStringLiteral("document-save")); } //END KateSaveConfigTab //BEGIN KateGotoBar KateGotoBar::KateGotoBar(KTextEditor::View *view, QWidget *parent) : KateViewBarWidget(true, parent) , m_view(view) { Q_ASSERT(m_view != nullptr); // this bar widget is pointless w/o a view QHBoxLayout *topLayout = new QHBoxLayout(centralWidget()); topLayout->setContentsMargins(0, 0, 0, 0); QToolButton *btn = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setText(i18n("&Line:")); btn->setToolTip(i18n("Go to line number from clipboard")); connect(btn, &QToolButton::clicked, this, &KateGotoBar::gotoClipboard); topLayout->addWidget(btn); m_gotoRange = new QSpinBox(this); m_gotoRange->setMinimum(1); topLayout->addWidget(m_gotoRange, 1); topLayout->setStretchFactor(m_gotoRange, 0); btn = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setText(i18n("Go to")); btn->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); connect(btn, &QToolButton::clicked, this, &KateGotoBar::gotoLine); topLayout->addWidget(btn); btn = m_modifiedUp = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setDefaultAction(m_view->action("modified_line_up")); btn->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search"))); btn->setText(QString()); btn->installEventFilter(this); topLayout->addWidget(btn); btn = m_modifiedDown = new QToolButton(this); btn->setAutoRaise(true); btn->setMinimumSize(QSize(1, btn->minimumSizeHint().height())); btn->setDefaultAction(m_view->action("modified_line_down")); btn->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); btn->setText(QString()); btn->installEventFilter(this); topLayout->addWidget(btn); topLayout->addStretch(); setFocusProxy(m_gotoRange); } void KateGotoBar::showEvent(QShowEvent *event) { Q_UNUSED(event) // Catch rare cases where the bar is visible while document is edited connect(m_view->document(), &KTextEditor::Document::textChanged, this, &KateGotoBar::updateData); } void KateGotoBar::closed() { disconnect(m_view->document(), &KTextEditor::Document::textChanged, this, &KateGotoBar::updateData); } bool KateGotoBar::eventFilter(QObject *object, QEvent *event) { if (object == m_modifiedUp || object == m_modifiedDown) { if (event->type() != QEvent::Wheel) { return false; } int delta = static_cast(event)->delta(); // Reset m_wheelDelta when scroll direction change if (m_wheelDelta != 0 && (m_wheelDelta < 0) != (delta < 0)) { m_wheelDelta = 0; } m_wheelDelta += delta; if (m_wheelDelta >= 120) { m_wheelDelta = 0; m_modifiedUp->click(); } else if (m_wheelDelta <= -120) { m_wheelDelta = 0; m_modifiedDown->click(); } } return false; } void KateGotoBar::gotoClipboard() { QRegularExpression rx(QStringLiteral("\\d+")); int lineNo = rx.match(QApplication::clipboard()->text(QClipboard::Selection)).captured().toInt(); if (lineNo <= m_gotoRange->maximum() && lineNo >= 1) { m_gotoRange->setValue(lineNo); gotoLine(); } else { QPointer message = new KTextEditor::Message( i18n("No valid line number found in clipboard")); message->setWordWrap(true); message->setAutoHide(2000); message->setPosition(KTextEditor::Message::BottomInView); message->setView(m_view), m_view->document()->postMessage(message); } } void KateGotoBar::updateData() { m_gotoRange->setMaximum(m_view->document()->lines()); if (!isVisible()) { m_gotoRange->setValue(m_view->cursorPosition().line() + 1); m_gotoRange->adjustSize(); // ### does not respect the range :-( } m_gotoRange->selectAll(); } void KateGotoBar::keyPressEvent(QKeyEvent *event) { int key = event->key(); if (key == Qt::Key_Return || key == Qt::Key_Enter) { gotoLine(); return; } KateViewBarWidget::keyPressEvent(event); } void KateGotoBar::gotoLine() { KTextEditor::ViewPrivate *kv = qobject_cast(m_view); if (kv && kv->selection() && !kv->config()->persistentSelection()) { kv->clearSelection(); } m_view->setCursorPosition(KTextEditor::Cursor(m_gotoRange->value() - 1, 0)); m_view->setFocus(); emit hideMe(); } //END KateGotoBar //BEGIN KateDictionaryBar KateDictionaryBar::KateDictionaryBar(KTextEditor::ViewPrivate *view, QWidget *parent) : KateViewBarWidget(true, parent) , m_view(view) { Q_ASSERT(m_view != nullptr); // this bar widget is pointless w/o a view QHBoxLayout *topLayout = new QHBoxLayout(centralWidget()); topLayout->setContentsMargins(0, 0, 0, 0); //topLayout->setSpacing(spacingHint()); m_dictionaryComboBox = new Sonnet::DictionaryComboBox(centralWidget()); connect(m_dictionaryComboBox, SIGNAL(dictionaryChanged(QString)), this, SLOT(dictionaryChanged(QString))); connect(view->doc(), SIGNAL(defaultDictionaryChanged(KTextEditor::DocumentPrivate*)), this, SLOT(updateData())); QLabel *label = new QLabel(i18n("Dictionary:"), centralWidget()); label->setBuddy(m_dictionaryComboBox); topLayout->addWidget(label); topLayout->addWidget(m_dictionaryComboBox, 1); topLayout->setStretchFactor(m_dictionaryComboBox, 0); topLayout->addStretch(); } KateDictionaryBar::~KateDictionaryBar() { } void KateDictionaryBar::updateData() { KTextEditor::DocumentPrivate *document = m_view->doc(); QString dictionary = document->defaultDictionary(); if (dictionary.isEmpty()) { dictionary = Sonnet::Speller().defaultLanguage(); } m_dictionaryComboBox->setCurrentByDictionary(dictionary); } void KateDictionaryBar::dictionaryChanged(const QString &dictionary) { const KTextEditor::Range selection = m_view->selectionRange(); if (selection.isValid() && !selection.isEmpty()) { const bool blockmode = m_view->blockSelection(); m_view->doc()->setDictionary(dictionary, selection, blockmode); } else { m_view->doc()->setDefaultDictionary(dictionary); } } //END KateGotoBar //BEGIN KateModOnHdPrompt KateModOnHdPrompt::KateModOnHdPrompt(KTextEditor::DocumentPrivate *doc, KTextEditor::ModificationInterface::ModifiedOnDiskReason modtype, const QString &reason) : QObject(doc) , m_doc(doc) , m_modtype(modtype) , m_proc(nullptr) , m_diffFile(nullptr) , m_diffAction(nullptr) { m_message = new KTextEditor::Message(reason, KTextEditor::Message::Information); m_message->setPosition(KTextEditor::Message::AboveView); m_message->setWordWrap(true); // If the file isn't deleted, present a diff button const bool onDiskDeleted = modtype == KTextEditor::ModificationInterface::OnDiskDeleted; if (!onDiskDeleted) { QAction * aAutoReload = new QAction(i18n("Enable Auto Reload"), this); aAutoReload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); aAutoReload->setToolTip(i18n("Will never again warn about on disk changes but always reload.")); m_message->addAction(aAutoReload, false); connect(aAutoReload, &QAction::triggered, this, &KateModOnHdPrompt::autoReloadTriggered); if (!QStandardPaths::findExecutable(QStringLiteral("diff")).isEmpty()) { m_diffAction = new QAction(i18n("View &Difference"), this); m_diffAction->setToolTip(i18n("Shows a diff of the changes")); m_message->addAction(m_diffAction, false); connect(m_diffAction, SIGNAL(triggered()), this, SLOT(slotDiff())); } QAction * aReload = new QAction(i18n("&Reload"), this); aReload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); aReload->setToolTip(i18n("Reload the file from disk. Unsaved changes will be lost.")); m_message->addAction(aReload); connect(aReload, SIGNAL(triggered()), this, SIGNAL(reloadTriggered())); } else { QAction * closeFile = new QAction(i18nc("@action:button closes the opened file", "&Close File"), this); closeFile->setIcon(QIcon::fromTheme(QStringLiteral("document-close"))); closeFile->setToolTip(i18n("Close the file, discarding its content.")); m_message->addAction(closeFile, false); connect(closeFile, &QAction::triggered, this, &KateModOnHdPrompt::closeTriggered); QAction * aSaveAs = new QAction(i18n("&Save As..."), this); aSaveAs->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); aSaveAs->setToolTip(i18n("Lets you select a location and save the file again.")); m_message->addAction(aSaveAs, false); connect(aSaveAs, SIGNAL(triggered()), this, SIGNAL(saveAsTriggered())); } QAction * aIgnore = new QAction(i18n("&Ignore"), this); aIgnore->setToolTip(i18n("Ignores the changes on disk without any action.")); aIgnore->setIcon(KStandardGuiItem::overwrite().icon()); m_message->addAction(aIgnore); connect(aIgnore, SIGNAL(triggered()), this, SIGNAL(ignoreTriggered())); m_doc->postMessage(m_message); } KateModOnHdPrompt::~KateModOnHdPrompt() { delete m_proc; m_proc = nullptr; if (m_diffFile) { m_diffFile->setAutoRemove(true); delete m_diffFile; m_diffFile = nullptr; } delete m_message; } void KateModOnHdPrompt::slotDiff() { if (m_diffFile) { return; } m_diffFile = new QTemporaryFile(); m_diffFile->open(); // Start a KProcess that creates a diff m_proc = new KProcess(this); m_proc->setOutputChannelMode(KProcess::MergedChannels); - *m_proc << QStringLiteral("diff") << QLatin1String("-u") + *m_proc << QStringLiteral("diff") << QStringLiteral("-u") << QStringLiteral("-") << m_doc->url().toLocalFile(); connect(m_proc, SIGNAL(readyRead()), this, SLOT(slotDataAvailable())); connect(m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotPDone())); // disable the diff button, to hinder the user to run it twice. m_diffAction->setEnabled(false); m_proc->start(); QTextStream ts(m_proc); int lastln = m_doc->lines() - 1; for (int l = 0; l < lastln; ++l) { ts << m_doc->line(l) << '\n'; } ts << m_doc->line(lastln); ts.flush(); m_proc->closeWriteChannel(); } void KateModOnHdPrompt::slotDataAvailable() { m_diffFile->write(m_proc->readAll()); } void KateModOnHdPrompt::slotPDone() { m_diffAction->setEnabled(true); const QProcess::ExitStatus es = m_proc->exitStatus(); delete m_proc; m_proc = nullptr; if (es != QProcess::NormalExit) { KMessageBox::sorry(nullptr, i18n("The diff command failed. Please make sure that " "diff(1) is installed and in your PATH."), i18n("Error Creating Diff")); delete m_diffFile; m_diffFile = nullptr; return; } if (m_diffFile->size() == 0) { KMessageBox::information(nullptr, i18n("The files are identical."), i18n("Diff Output")); delete m_diffFile; m_diffFile = nullptr; return; } m_diffFile->setAutoRemove(false); QUrl url = QUrl::fromLocalFile(m_diffFile->fileName()); delete m_diffFile; m_diffFile = nullptr; // KRun::runUrl should delete the file, once the client exits KRun::runUrl(url, QStringLiteral("text/x-patch"), nullptr, KRun::RunFlags(KRun::DeleteTemporaryFiles)); } //END KateModOnHdPrompt diff --git a/src/document/katedocument.cpp b/src/document/katedocument.cpp index a14da0ab..e23a8162 100644 --- a/src/document/katedocument.cpp +++ b/src/document/katedocument.cpp @@ -1,6085 +1,6085 @@ /* This file is part of the KDE libraries Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2006 Hamish Rodda Copyright (C) 2007 Mirko Stocker Copyright (C) 2009-2010 Michel Ludwig Copyright (C) 2013 Gerald Senarclens de Grancy Copyright (C) 2013 Andrey Matveyakin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-13020, USA. */ //BEGIN includes #include "config.h" #include "katedocument.h" #include "kateglobal.h" #include "katedialogs.h" #include "katehighlight.h" #include "kateview.h" #include "kateautoindent.h" #include "katetextline.h" #include "katerenderer.h" #include "kateregexp.h" #include "kateplaintextsearch.h" #include "kateregexpsearch.h" #include "kateconfig.h" #include "katemodemanager.h" #include "kateschema.h" #include "katebuffer.h" #include "kateundomanager.h" #include "spellcheck/prefixstore.h" #include "spellcheck/ontheflycheck.h" #include "spellcheck/spellcheck.h" #include "katescriptmanager.h" #include "kateswapfile.h" #include "katepartdebug.h" #include "printing/kateprinter.h" #include "kateabstractinputmode.h" #include "katetemplatehandler.h" #if EDITORCONFIG_FOUND #include "editorconfig.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if LIBGIT2_FOUND #include #include #include #endif //END includes #if 0 #define EDIT_DEBUG qCDebug(LOG_KTE) #else #define EDIT_DEBUG if (0) qCDebug(LOG_KTE) #endif template static int indexOf(const std::initializer_list & list, const E& entry) { auto it = std::find(list.begin(), list.end(), entry); return it == list.end() ? -1 : std::distance(list.begin(), it); } template static bool contains(const std::initializer_list & list, const E& entry) { return indexOf(list, entry) >= 0; } static inline QChar matchingStartBracket(const QChar c) { switch (c.toLatin1()) { case '}': return QLatin1Char('{'); case ']': return QLatin1Char('['); case ')': return QLatin1Char('('); } return QChar(); } static inline QChar matchingEndBracket(const QChar c, bool withQuotes = true) { switch (c.toLatin1()) { case '{': return QLatin1Char('}'); case '[': return QLatin1Char(']'); case '(': return QLatin1Char(')'); case '\'': return withQuotes ? QLatin1Char('\'') : QChar(); case '"': return withQuotes ? QLatin1Char('"') : QChar(); } return QChar(); } static inline QChar matchingBracket(const QChar c) { QChar bracket = matchingStartBracket(c); if (bracket.isNull()) { bracket = matchingEndBracket(c, /*withQuotes=*/false); } return bracket; } static inline bool isStartBracket(const QChar c) { return ! matchingEndBracket(c, /*withQuotes=*/false).isNull(); } static inline bool isEndBracket(const QChar c) { return ! matchingStartBracket(c).isNull(); } static inline bool isBracket(const QChar c) { return isStartBracket(c) || isEndBracket(c); } /** * normalize given url * @param url input url * @return normalized url */ static QUrl normalizeUrl (const QUrl &url) { /** * only normalize local urls */ if (url.isEmpty() || !url.isLocalFile()) return url; /** * don't normalize if not existing! * canonicalFilePath won't work! */ const QString normalizedUrl(QFileInfo(url.toLocalFile()).canonicalFilePath()); if (normalizedUrl.isEmpty()) return url; /** * else: use canonicalFilePath to normalize */ return QUrl::fromLocalFile(normalizedUrl); } //BEGIN d'tor, c'tor // // KTextEditor::DocumentPrivate Constructor // KTextEditor::DocumentPrivate::DocumentPrivate(bool bSingleViewMode, bool bReadOnly, QWidget *parentWidget, QObject *parent) : KTextEditor::Document (this, parent), m_bSingleViewMode(bSingleViewMode), m_bReadOnly(bReadOnly), m_undoManager(new KateUndoManager(this)), m_buffer(new KateBuffer(this)), m_indenter(new KateAutoIndent(this)), m_docName(QStringLiteral("need init")), m_fileType(QStringLiteral("Normal")), m_config(new KateDocumentConfig(this)) { /** * no plugins from kparts here */ setPluginLoadingMode (DoNotLoadPlugins); /** * pass on our component data, do this after plugin loading is off */ setComponentData(KTextEditor::EditorPrivate::self()->aboutData()); /** * avoid spamming plasma and other window managers with progress dialogs * we show such stuff inline in the views! */ setProgressInfoEnabled(false); // register doc at factory KTextEditor::EditorPrivate::self()->registerDocument(this); // normal hl m_buffer->setHighlight(0); // swap file m_swapfile = (config()->swapFileMode() == KateDocumentConfig::DisableSwapFile) ? nullptr : new Kate::SwapFile(this); // some nice signals from the buffer connect(m_buffer, SIGNAL(tagLines(int,int)), this, SLOT(tagLines(int,int))); // if the user changes the highlight with the dialog, notify the doc connect(KateHlManager::self(), SIGNAL(changed()), SLOT(internalHlChanged())); // signals for mod on hd connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(dirty(QString)), this, SLOT(slotModOnHdDirty(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(created(QString)), this, SLOT(slotModOnHdCreated(QString))); connect(KTextEditor::EditorPrivate::self()->dirWatch(), SIGNAL(deleted(QString)), this, SLOT(slotModOnHdDeleted(QString))); /** * singleshot timer to handle updates of mod on hd state delayed */ m_modOnHdTimer.setSingleShot(true); m_modOnHdTimer.setInterval(200); connect(&m_modOnHdTimer, SIGNAL(timeout()), this, SLOT(slotDelayedHandleModOnHd())); // Setup auto reload stuff m_autoReloadMode = new KToggleAction(i18n("Auto Reload Document"), this); m_autoReloadMode->setWhatsThis(i18n("Automatic reload the document when it was changed on disk")); connect(m_autoReloadMode, &KToggleAction::triggered, this, &DocumentPrivate::autoReloadToggled); // Prepare some reload amok protector... m_autoReloadThrottle.setSingleShot(true); //...but keep the value small in unit tests m_autoReloadThrottle.setInterval(KTextEditor::EditorPrivate::self()->unitTestMode() ? 50 : 3000); connect(&m_autoReloadThrottle, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); /** * load handling * this is needed to ensure we signal the user if a file ist still loading * and to disallow him to edit in that time */ connect(this, SIGNAL(started(KIO::Job*)), this, SLOT(slotStarted(KIO::Job*))); connect(this, SIGNAL(completed()), this, SLOT(slotCompleted())); connect(this, SIGNAL(canceled(QString)), this, SLOT(slotCanceled())); connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(slotUrlChanged(QUrl))); // update doc name updateDocName(); // if single view mode, like in the konqui embedding, create a default view ;) // be lazy, only create it now, if any parentWidget is given, otherwise widget() // will create it on demand... if (m_bSingleViewMode && parentWidget) { KTextEditor::View *view = (KTextEditor::View *)createView(parentWidget); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); } connect(m_undoManager, SIGNAL(undoChanged()), this, SIGNAL(undoChanged())); connect(m_undoManager, SIGNAL(undoStart(KTextEditor::Document*)), this, SIGNAL(editingStarted(KTextEditor::Document*))); connect(m_undoManager, SIGNAL(undoEnd(KTextEditor::Document*)), this, SIGNAL(editingFinished(KTextEditor::Document*))); connect(m_undoManager, SIGNAL(redoStart(KTextEditor::Document*)), this, SIGNAL(editingStarted(KTextEditor::Document*))); connect(m_undoManager, SIGNAL(redoEnd(KTextEditor::Document*)), this, SIGNAL(editingFinished(KTextEditor::Document*))); connect(this, SIGNAL(sigQueryClose(bool*,bool*)), this, SLOT(slotQueryClose_save(bool*,bool*))); connect(this, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearEditingPosStack())); onTheFlySpellCheckingEnabled(config()->onTheFlySpellCheck()); // make sure correct defaults are set (indenter, ...) updateConfig(); } // // KTextEditor::DocumentPrivate Destructor // KTextEditor::DocumentPrivate::~DocumentPrivate() { // delete pending mod-on-hd message, if applicable delete m_modOnHdHandler; /** * we are about to delete cursors/ranges/... */ emit aboutToDeleteMovingInterfaceContent(this); // kill it early, it has ranges! delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; clearDictionaryRanges(); // Tell the world that we're about to close (== destruct) // Apps must receive this in a direct signal-slot connection, and prevent // any further use of interfaces once they return. emit aboutToClose(this); // remove file from dirwatch deactivateDirWatch(); // thanks for offering, KPart, but we're already self-destructing setAutoDeleteWidget(false); setAutoDeletePart(false); // clean up remaining views qDeleteAll (m_views.keys()); m_views.clear(); // cu marks for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { delete i.value(); } m_marks.clear(); delete m_config; KTextEditor::EditorPrivate::self()->deregisterDocument(this); } //END void KTextEditor::DocumentPrivate::saveEditingPositions(const KTextEditor::Cursor &cursor) { if (m_editingStackPosition != m_editingStack.size() - 1) { m_editingStack.resize(m_editingStackPosition); } // try to be clever: reuse existing cursors if possible QSharedPointer mc; // we might pop last one: reuse that if (!m_editingStack.isEmpty() && cursor.line() == m_editingStack.top()->line()) { mc = m_editingStack.pop(); } // we might expire oldest one, reuse that one, if not already one there // we prefer the other one for reuse, as already on the right line aka in the right block! const int editingStackSizeLimit = 32; if (m_editingStack.size() >= editingStackSizeLimit) { if (mc) { m_editingStack.removeFirst(); } else { mc = m_editingStack.takeFirst(); } } // new cursor needed? or adjust existing one? if (mc) { mc->setPosition(cursor); } else { mc = QSharedPointer (newMovingCursor(cursor)); } // add new one as top of stack m_editingStack.push(mc); m_editingStackPosition = m_editingStack.size() - 1; } KTextEditor::Cursor KTextEditor::DocumentPrivate::lastEditingPosition(EditingPositionKind nextOrPrev, KTextEditor::Cursor currentCursor) { if (m_editingStack.isEmpty()) { return KTextEditor::Cursor::invalid(); } auto targetPos = m_editingStack.at(m_editingStackPosition)->toCursor(); if (targetPos == currentCursor) { if (nextOrPrev == Previous) { m_editingStackPosition--; } else { m_editingStackPosition++; } m_editingStackPosition = qBound(0, m_editingStackPosition, m_editingStack.size() - 1); } return m_editingStack.at(m_editingStackPosition)->toCursor(); } void KTextEditor::DocumentPrivate::clearEditingPosStack() { m_editingStack.clear(); m_editingStackPosition = -1; } // on-demand view creation QWidget *KTextEditor::DocumentPrivate::widget() { // no singleViewMode -> no widget()... if (!singleViewMode()) { return nullptr; } // does a widget exist already? use it! if (KTextEditor::Document::widget()) { return KTextEditor::Document::widget(); } // create and return one... KTextEditor::View *view = (KTextEditor::View *)createView(nullptr); insertChildClient(view); view->setContextMenu(view->defaultContextMenu()); setWidget(view); return view; } //BEGIN KTextEditor::Document stuff KTextEditor::View *KTextEditor::DocumentPrivate::createView(QWidget *parent, KTextEditor::MainWindow *mainWindow) { KTextEditor::ViewPrivate *newView = new KTextEditor::ViewPrivate(this, parent, mainWindow); if (m_fileChangedDialogsActivated) { connect(newView, SIGNAL(focusIn(KTextEditor::View*)), this, SLOT(slotModifiedOnDisk())); } emit viewCreated(this, newView); // post existing messages to the new view, if no specific view is given foreach (KTextEditor::Message *message, m_messageHash.keys()) { if (!message->view()) { newView->postMessage(message, m_messageHash[message]); } } return newView; } KTextEditor::Range KTextEditor::DocumentPrivate::rangeOnLine(KTextEditor::Range range, int line) const { const int col1 = toVirtualColumn(range.start()); const int col2 = toVirtualColumn(range.end()); return KTextEditor::Range(line, fromVirtualColumn(line, col1), line, fromVirtualColumn(line, col2)); } //BEGIN KTextEditor::EditInterface stuff bool KTextEditor::DocumentPrivate::isEditingTransactionRunning() const { return editSessionNumber > 0; } QString KTextEditor::DocumentPrivate::text() const { return m_buffer->text(); } QString KTextEditor::DocumentPrivate::text(const KTextEditor::Range &range, bool blockwise) const { if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return QString(); } QString s; if (range.start().line() == range.end().line()) { if (range.start().column() > range.end().column()) { return QString(); } Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return QString(); } return textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { s.append(textLine->string(range.start().column(), textLine->length() - range.start().column())); } else if (i == range.end().line()) { s.append(textLine->string(0, range.end().column())); } else { s.append(textLine->string()); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); s.append(textLine->string(subRange.start().column(), subRange.columnWidth())); } if (i < range.end().line()) { s.append(QLatin1Char('\n')); } } } return s; } QChar KTextEditor::DocumentPrivate::characterAt(const KTextEditor::Cursor &position) const { Kate::TextLine textLine = m_buffer->plainLine(position.line()); if (!textLine) { return QChar(); } return textLine->at(position.column()); } QString KTextEditor::DocumentPrivate::wordAt(const KTextEditor::Cursor &cursor) const { return text(wordRangeAt(cursor)); } KTextEditor::Range KTextEditor::DocumentPrivate::wordRangeAt(const KTextEditor::Cursor &cursor) const { // get text line const int line = cursor.line(); Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { return KTextEditor::Range::invalid(); } // make sure the cursor is const int lineLenth = textLine->length(); if (cursor.column() > lineLenth) { return KTextEditor::Range::invalid(); } int start = cursor.column(); int end = start; while (start > 0 && highlight()->isInWord(textLine->at(start - 1), textLine->attribute(start - 1))) { start--; } while (end < lineLenth && highlight()->isInWord(textLine->at(end), textLine->attribute(end))) { end++; } return KTextEditor::Range(line, start, line, end); } bool KTextEditor::DocumentPrivate::isValidTextPosition(const KTextEditor::Cursor& cursor) const { const int ln = cursor.line(); const int col = cursor.column(); // cursor in document range? if (ln < 0 || col < 0 || ln >= lines() || col > lineLength(ln)) { return false; } const QString str = line(ln); Q_ASSERT(str.length() >= col); // cursor at end of line? const int len = lineLength(ln); if (col == 0 || col == len) { return true; } // cursor in the middle of a valid utf32-surrogate? return (! str.at(col).isLowSurrogate()) || (! str.at(col-1).isHighSurrogate()); } QStringList KTextEditor::DocumentPrivate::textLines(const KTextEditor::Range &range, bool blockwise) const { QStringList ret; if (!range.isValid()) { qCWarning(LOG_KTE) << "Text requested for invalid range" << range; return ret; } if (blockwise && (range.start().column() > range.end().column())) { return ret; } if (range.start().line() == range.end().line()) { Q_ASSERT(range.start() <= range.end()); Kate::TextLine textLine = m_buffer->plainLine(range.start().line()); if (!textLine) { return ret; } ret << textLine->string(range.start().column(), range.end().column() - range.start().column()); } else { for (int i = range.start().line(); (i <= range.end().line()) && (i < m_buffer->count()); ++i) { Kate::TextLine textLine = m_buffer->plainLine(i); if (!blockwise) { if (i == range.start().line()) { ret << textLine->string(range.start().column(), textLine->length() - range.start().column()); } else if (i == range.end().line()) { ret << textLine->string(0, range.end().column()); } else { ret << textLine->string(); } } else { KTextEditor::Range subRange = rangeOnLine(range, i); ret << textLine->string(subRange.start().column(), subRange.columnWidth()); } } } return ret; } QString KTextEditor::DocumentPrivate::line(int line) const { Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return QString(); } return l->string(); } bool KTextEditor::DocumentPrivate::setText(const QString &s) { if (!isReadWrite()) { return false; } QList msave; foreach (KTextEditor::Mark *mark, m_marks) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor(), s); editEnd(); foreach (KTextEditor::Mark mark, msave) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::setText(const QStringList &text) { if (!isReadWrite()) { return false; } QList msave; foreach (KTextEditor::Mark *mark, m_marks) { msave.append(*mark); } editStart(); // delete the text clear(); // insert the new text insertText(KTextEditor::Cursor::start(), text); editEnd(); foreach (KTextEditor::Mark mark, msave) { setMark(mark.line, mark.type); } return true; } bool KTextEditor::DocumentPrivate::clear() { if (!isReadWrite()) { return false; } foreach (KTextEditor::ViewPrivate *view, m_views) { view->clear(); view->tagAll(); view->update(); } clearMarks(); emit aboutToInvalidateMovingInterfaceContent(this); m_buffer->invalidateRanges(); emit aboutToRemoveText(documentRange()); return editRemoveLines(0, lastLine()); } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QString &text, bool block) { if (!isReadWrite()) { return false; } if (text.isEmpty()) { return true; } editStart(); int currentLine = position.line(); int currentLineStart = 0; const int totalLength = text.length(); int insertColumn = position.column(); // pad with empty lines, if insert position is after last line if (position.line() > lines()) { int line = lines(); while (line <= position.line()) { editInsertLine(line, QString()); line++; } } // compute expanded column for block mode int positionColumnExpanded = insertColumn; const int tabWidth = config()->tabWidth(); if (block) { if (auto l = plainKateTextLine(currentLine)) { positionColumnExpanded = l->toVirtualColumn(insertColumn, tabWidth); } } int pos = 0; for (; pos < totalLength; pos++) { const QChar &ch = text.at(pos); if (ch == QLatin1Char('\n')) { // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } if (!block) { editWrapLine(currentLine, insertColumn + pos - currentLineStart); insertColumn = 0; } currentLine++; if (block) { auto l = plainKateTextLine(currentLine); if (currentLine == lastLine() + 1) { editInsertLine(currentLine, QString()); } insertColumn = positionColumnExpanded; if (l) { insertColumn = l->fromVirtualColumn(insertColumn, tabWidth); } } currentLineStart = pos + 1; } } // Only perform the text insert if there is text to insert if (currentLineStart < pos) { editInsertText(currentLine, insertColumn, text.mid(currentLineStart, pos - currentLineStart)); } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertText(const KTextEditor::Cursor &position, const QStringList &textLines, bool block) { if (!isReadWrite()) { return false; } // just reuse normal function return insertText(position, textLines.join(QLatin1Char('\n')), block); } bool KTextEditor::DocumentPrivate::removeText(const KTextEditor::Range &_range, bool block) { KTextEditor::Range range = _range; if (!isReadWrite()) { return false; } // Should now be impossible to trigger with the new Range class Q_ASSERT(range.start().line() <= range.end().line()); if (range.start().line() > lastLine()) { return false; } if (!block) { emit aboutToRemoveText(range); } editStart(); if (!block) { if (range.end().line() > lastLine()) { range.setEnd(KTextEditor::Cursor(lastLine() + 1, 0)); } if (range.onSingleLine()) { editRemoveText(range.start().line(), range.start().column(), range.columnWidth()); } else { int from = range.start().line(); int to = range.end().line(); // remove last line if (to <= lastLine()) { editRemoveText(to, 0, range.end().column()); } // editRemoveLines() will be called on first line (to remove bookmark) if (range.start().column() == 0 && from > 0) { --from; } // remove middle lines editRemoveLines(from + 1, to - 1); // remove first line if not already removed by editRemoveLines() if (range.start().column() > 0 || range.start().line() == 0) { editRemoveText(from, range.start().column(), m_buffer->plainLine(from)->length() - range.start().column()); editUnWrapLine(from); } } } // if ( ! block ) else { int startLine = qMax(0, range.start().line()); int vc1 = toVirtualColumn(range.start()); int vc2 = toVirtualColumn(range.end()); for (int line = qMin(range.end().line(), lastLine()); line >= startLine; --line) { int col1 = fromVirtualColumn(line, vc1); int col2 = fromVirtualColumn(line, vc2); editRemoveText(line, qMin(col1, col2), qAbs(col2 - col1)); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::insertLine(int l, const QString &str) { if (!isReadWrite()) { return false; } if (l < 0 || l > lines()) { return false; } return editInsertLine(l, str); } bool KTextEditor::DocumentPrivate::insertLines(int line, const QStringList &text) { if (!isReadWrite()) { return false; } if (line < 0 || line > lines()) { return false; } bool success = true; foreach (const QString &string, text) { success &= editInsertLine(line++, string); } return success; } bool KTextEditor::DocumentPrivate::removeLine(int line) { if (!isReadWrite()) { return false; } if (line < 0 || line > lastLine()) { return false; } return editRemoveLine(line); } int KTextEditor::DocumentPrivate::totalCharacters() const { int l = 0; for (int i = 0; i < m_buffer->count(); ++i) { Kate::TextLine line = m_buffer->plainLine(i); if (line) { l += line->length(); } } return l; } int KTextEditor::DocumentPrivate::lines() const { return m_buffer->count(); } int KTextEditor::DocumentPrivate::lineLength(int line) const { if (line < 0 || line > lastLine()) { return -1; } Kate::TextLine l = m_buffer->plainLine(line); if (!l) { return -1; } return l->length(); } bool KTextEditor::DocumentPrivate::isLineModified(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified(); } bool KTextEditor::DocumentPrivate::isLineSaved(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsSavedOnDisk(); } bool KTextEditor::DocumentPrivate::isLineTouched(int line) const { if (line < 0 || line >= lines()) { return false; } Kate::TextLine l = m_buffer->plainLine(line); Q_ASSERT(l); return l->markedAsModified() || l->markedAsSavedOnDisk(); } //END //BEGIN KTextEditor::EditInterface internal stuff // // Starts an edit session with (or without) undo, update of view disabled during session // bool KTextEditor::DocumentPrivate::editStart() { editSessionNumber++; if (editSessionNumber > 1) { return false; } editIsRunning = true; // no last change cursor at start m_editLastChangeStartCursor = KTextEditor::Cursor::invalid(); m_undoManager->editStart(); foreach (KTextEditor::ViewPrivate *view, m_views) { view->editStart(); } m_buffer->editStart(); return true; } // // End edit session and update Views // bool KTextEditor::DocumentPrivate::editEnd() { if (editSessionNumber == 0) { Q_ASSERT(0); return false; } // wrap the new/changed text, if something really changed! if (m_buffer->editChanged() && (editSessionNumber == 1)) if (m_undoManager->isActive() && config()->wordWrap()) { wrapText(m_buffer->editTagStart(), m_buffer->editTagEnd()); } editSessionNumber--; if (editSessionNumber > 0) { return false; } // end buffer edit, will trigger hl update // this will cause some possible adjustment of tagline start/end m_buffer->editEnd(); m_undoManager->editEnd(); // edit end for all views !!!!!!!!! foreach (KTextEditor::ViewPrivate *view, m_views) { view->editEnd(m_buffer->editTagStart(), m_buffer->editTagEnd(), m_buffer->editTagFrom()); } if (m_buffer->editChanged()) { setModified(true); emit textChanged(this); } // remember last change position in the stack, if any // this avoid costly updates for longer editing transactions // before we did that on textInsert/Removed if (m_editLastChangeStartCursor.isValid()) saveEditingPositions(m_editLastChangeStartCursor); editIsRunning = false; return true; } void KTextEditor::DocumentPrivate::pushEditState() { editStateStack.push(editSessionNumber); } void KTextEditor::DocumentPrivate::popEditState() { if (editStateStack.isEmpty()) { return; } int count = editStateStack.pop() - editSessionNumber; while (count < 0) { ++count; editEnd(); } while (count > 0) { --count; editStart(); } } void KTextEditor::DocumentPrivate::inputMethodStart() { m_undoManager->inputMethodStart(); } void KTextEditor::DocumentPrivate::inputMethodEnd() { m_undoManager->inputMethodEnd(); } bool KTextEditor::DocumentPrivate::wrapText(int startLine, int endLine) { if (startLine < 0 || endLine < 0) { return false; } if (!isReadWrite()) { return false; } int col = config()->wordWrapAt(); if (col == 0) { return false; } editStart(); for (int line = startLine; (line <= endLine) && (line < lines()); line++) { Kate::TextLine l = kateTextLine(line); if (!l) { break; } //qCDebug(LOG_KTE) << "try wrap line: " << line; if (l->virtualLength(m_buffer->tabWidth()) > col) { Kate::TextLine nextl = kateTextLine(line + 1); //qCDebug(LOG_KTE) << "do wrap line: " << line; int eolPosition = l->length() - 1; // take tabs into account here, too int x = 0; const QString &t = l->string(); int z2 = 0; for (; z2 < l->length(); z2++) { static const QChar tabChar(QLatin1Char('\t')); if (t.at(z2) == tabChar) { x += m_buffer->tabWidth() - (x % m_buffer->tabWidth()); } else { x++; } if (x > col) { break; } } const int colInChars = qMin(z2, l->length() - 1); int searchStart = colInChars; // If where we are wrapping is an end of line and is a space we don't // want to wrap there if (searchStart == eolPosition && t.at(searchStart).isSpace()) { searchStart--; } // Scan backwards looking for a place to break the line // We are not interested in breaking at the first char // of the line (if it is a space), but we are at the second // anders: if we can't find a space, try breaking on a word // boundary, using KateHighlight::canBreakAt(). // This could be a priority (setting) in the hl/filetype/document int z = -1; int nw = -1; // alternative position, a non word character for (z = searchStart; z >= 0; z--) { if (t.at(z).isSpace()) { break; } if ((nw < 0) && highlight()->canBreakAt(t.at(z), l->attribute(z))) { nw = z; } } if (z >= 0) { // So why don't we just remove the trailing space right away? // Well, the (view's) cursor may be directly in front of that space // (user typing text before the last word on the line), and if that // happens, the cursor would be moved to the next line, which is not // what we want (bug #106261) z++; } else { // There was no space to break at so break at a nonword character if // found, or at the wrapcolumn ( that needs be configurable ) // Don't try and add any white space for the break if ((nw >= 0) && nw < colInChars) { nw++; // break on the right side of the character } z = (nw >= 0) ? nw : colInChars; } if (nextl && !nextl->isAutoWrapped()) { editWrapLine(line, z, true); editMarkLineAutoWrapped(line + 1, true); endLine++; } else { if (nextl && (nextl->length() > 0) && !nextl->at(0).isSpace() && ((l->length() < 1) || !l->at(l->length() - 1).isSpace())) { - editInsertText(line + 1, 0, QLatin1String(" ")); + editInsertText(line + 1, 0, QStringLiteral(" ")); } bool newLineAdded = false; editWrapLine(line, z, false, &newLineAdded); editMarkLineAutoWrapped(line + 1, true); endLine++; } } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::wrapParagraph(int first, int last) { if (first == last) { return wrapText(first, last); } if (first < 0 || last < first) { return false; } if (last >= lines() || first > last) { return false; } if (!isReadWrite()) { return false; } editStart(); // Because we shrink and expand lines, we need to track the working set by powerful "MovingStuff" std::unique_ptr range(newMovingRange(KTextEditor::Range(first, 0, last, 0))); std::unique_ptr curr(newMovingCursor(KTextEditor::Cursor(range->start()))); // Scan the selected range for paragraphs, whereas each empty line trigger a new paragraph for (int line = first; line <= range->end().line(); ++line) { // Is our first line a somehow filled line? if(plainKateTextLine(first)->firstChar() < 0) { // Fast forward to first non empty line ++first; curr->setPosition(curr->line() + 1, 0); continue; } // Is our current line a somehow filled line? If not, wrap the paragraph if (plainKateTextLine(line)->firstChar() < 0) { curr->setPosition(line, 0); // Set on empty line joinLines(first, line - 1); // Don't wrap twice! That may cause a bad result if (!wordWrap()) { wrapText(first, first); } first = curr->line() + 1; line = first; } } // If there was no paragraph, we need to wrap now bool needWrap = (curr->line() != range->end().line()); if (needWrap && plainKateTextLine(first)->firstChar() != -1) { joinLines(first, range->end().line()); // Don't wrap twice! That may cause a bad result if (!wordWrap()) { wrapText(first, first); } } editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertText(int line, int col, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertText" << line << col << s; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (s.isEmpty()) { return true; } editStart(); QString s2 = s; int col2 = col; if (col2 > l->length()) { s2 = QString(col2 - l->length(), QLatin1Char(' ')) + s; col2 = l->length(); } m_undoManager->slotTextInserted(line, col2, s2); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col2); // insert text into line m_buffer->insertText(m_editLastChangeStartCursor, s2); emit textInserted(this, KTextEditor::Range(line, col2, line, col2 + s2.length())); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveText(int line, int col, int len) { // verbose debug EDIT_DEBUG << "editRemoveText" << line << col << len; if (line < 0 || col < 0 || len < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } // nothing to do, do nothing! if (len == 0) { return true; } // wrong column if (col >= l->text().size()) { return false; } // don't try to remove what's not there len = qMin(len, l->text().size() - col); editStart(); QString oldText = l->string().mid(col, len); m_undoManager->slotTextRemoved(line, col, oldText); // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); // remove text from line m_buffer->removeText(KTextEditor::Range(m_editLastChangeStartCursor, KTextEditor::Cursor(line, col + len))); emit textRemoved(this, KTextEditor::Range(line, col, line, col + len), oldText); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editMarkLineAutoWrapped(int line, bool autowrapped) { // verbose debug EDIT_DEBUG << "editMarkLineAutoWrapped" << line << autowrapped; if (line < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); m_undoManager->slotMarkLineAutoWrapped(line, autowrapped); l->setAutoWrapped(autowrapped); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editWrapLine(int line, int col, bool newLine, bool *newLineAdded) { // verbose debug EDIT_DEBUG << "editWrapLine" << line << col << newLine; if (line < 0 || col < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); if (!l) { return false; } editStart(); Kate::TextLine nextLine = kateTextLine(line + 1); const int length = l->length(); m_undoManager->slotLineWrapped(line, col, length - col, (!nextLine || newLine)); if (!nextLine || newLine) { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { if ((col == 0) || (i.value()->line > line)) { list.append(i.value()); } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // yes, we added a new line ! if (newLineAdded) { (*newLineAdded) = true; } } else { m_buffer->wrapLine(KTextEditor::Cursor(line, col)); m_buffer->unwrapLine(line + 2); // no, no new line added ! if (newLineAdded) { (*newLineAdded) = false; } } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textInserted(this, KTextEditor::Range(line, col, line + 1, 0)); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editUnWrapLine(int line, bool removeLine, int length) { // verbose debug EDIT_DEBUG << "editUnWrapLine" << line << removeLine << length; if (line < 0 || length < 0) { return false; } if (!isReadWrite()) { return false; } Kate::TextLine l = kateTextLine(line); Kate::TextLine nextLine = kateTextLine(line + 1); if (!l || !nextLine) { return false; } editStart(); int col = l->length(); m_undoManager->slotLineUnWrapped(line, col, length, removeLine); if (removeLine) { m_buffer->unwrapLine(line + 1); } else { m_buffer->wrapLine(KTextEditor::Cursor(line + 1, length)); m_buffer->unwrapLine(line + 1); } QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line + 1) { list.append(i.value()); } if (i.value()->line == line + 1) { KTextEditor::Mark *mark = m_marks.take(line); if (mark) { i.value()->type |= mark->type; } } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line--; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } // remember last change cursor m_editLastChangeStartCursor = KTextEditor::Cursor(line, col); emit textRemoved(this, KTextEditor::Range(line, col, line + 1, 0), QStringLiteral("\n")); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editInsertLine(int line, const QString &s) { // verbose debug EDIT_DEBUG << "editInsertLine" << line << s; if (line < 0) { return false; } if (!isReadWrite()) { return false; } if (line > lines()) { return false; } editStart(); m_undoManager->slotLineInserted(line, s); // wrap line if (line > 0) { Kate::TextLine previousLine = m_buffer->line(line - 1); m_buffer->wrapLine(KTextEditor::Cursor(line - 1, previousLine->text().size())); } else { m_buffer->wrapLine(KTextEditor::Cursor(0, 0)); } // insert text m_buffer->insertText(KTextEditor::Cursor(line, 0), s); Kate::TextLine tl = m_buffer->line(line); QList list; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { if (i.value()->line >= line) { list.append(i.value()); } } for (int i = 0; i < list.size(); ++i) { m_marks.take(list.at(i)->line); } for (int i = 0; i < list.size(); ++i) { list.at(i)->line++; m_marks.insert(list.at(i)->line, list.at(i)); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeInserted(line, 0, line, tl->length()); if (line) { Kate::TextLine prevLine = plainKateTextLine(line - 1); rangeInserted.setStart(KTextEditor::Cursor(line - 1, prevLine->length())); } else { rangeInserted.setEnd(KTextEditor::Cursor(line + 1, 0)); } // remember last change cursor m_editLastChangeStartCursor = rangeInserted.start(); emit textInserted(this, rangeInserted); editEnd(); return true; } bool KTextEditor::DocumentPrivate::editRemoveLine(int line) { return editRemoveLines(line, line); } bool KTextEditor::DocumentPrivate::editRemoveLines(int from, int to) { // verbose debug EDIT_DEBUG << "editRemoveLines" << from << to; if (to < from || from < 0 || to > lastLine()) { return false; } if (!isReadWrite()) { return false; } if (lines() == 1) { return editRemoveText(0, 0, kateTextLine(0)->length()); } editStart(); QStringList oldText; /** * first remove text */ for (int line = to; line >= from; --line) { Kate::TextLine tl = m_buffer->line(line); oldText.prepend(this->line(line)); m_undoManager->slotLineRemoved(line, this->line(line)); m_buffer->removeText(KTextEditor::Range(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, tl->text().size()))); } /** * then collapse lines */ for (int line = to; line >= from; --line) { /** * unwrap all lines, prefer to unwrap line behind, skip to wrap line 0 */ if (line + 1 < m_buffer->lines()) { m_buffer->unwrapLine(line + 1); } else if (line) { m_buffer->unwrapLine(line); } } QList rmark; QList list; foreach (KTextEditor::Mark *mark, m_marks) { int line = mark->line; if (line > to) { list << line; } else if (line >= from) { rmark << line; } } foreach (int line, rmark) { delete m_marks.take(line); } foreach (int line, list) { KTextEditor::Mark *mark = m_marks.take(line); mark->line -= to - from + 1; m_marks.insert(mark->line, mark); } if (!list.isEmpty()) { emit marksChanged(this); } KTextEditor::Range rangeRemoved(from, 0, to + 1, 0); if (to == lastLine() + to - from + 1) { rangeRemoved.setEnd(KTextEditor::Cursor(to, oldText.last().length())); if (from > 0) { Kate::TextLine prevLine = plainKateTextLine(from - 1); rangeRemoved.setStart(KTextEditor::Cursor(from - 1, prevLine->length())); } } // remember last change cursor m_editLastChangeStartCursor = rangeRemoved.start(); emit textRemoved(this, rangeRemoved, oldText.join(QLatin1Char('\n')) + QLatin1Char('\n')); editEnd(); return true; } //END //BEGIN KTextEditor::UndoInterface stuff uint KTextEditor::DocumentPrivate::undoCount() const { return m_undoManager->undoCount(); } uint KTextEditor::DocumentPrivate::redoCount() const { return m_undoManager->redoCount(); } void KTextEditor::DocumentPrivate::undo() { m_undoManager->undo(); } void KTextEditor::DocumentPrivate::redo() { m_undoManager->redo(); } //END //BEGIN KTextEditor::SearchInterface stuff QVector KTextEditor::DocumentPrivate::searchText( const KTextEditor::Range &range, const QString &pattern, const KTextEditor::SearchOptions options) const { const bool escapeSequences = options.testFlag(KTextEditor::EscapeSequences); const bool regexMode = options.testFlag(KTextEditor::Regex); const bool backwards = options.testFlag(KTextEditor::Backwards); const bool wholeWords = options.testFlag(KTextEditor::WholeWords); const Qt::CaseSensitivity caseSensitivity = options.testFlag(KTextEditor::CaseInsensitive) ? Qt::CaseInsensitive : Qt::CaseSensitive; if (regexMode) { // regexp search // escape sequences are supported by definition KateRegExpSearch searcher(this, caseSensitivity); return searcher.search(pattern, range, backwards); } if (escapeSequences) { // escaped search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(KateRegExpSearch::escapePlaintext(pattern), range, backwards); QVector result; result.append(match); return result; } // plaintext search KatePlainTextSearch searcher(this, caseSensitivity, wholeWords); KTextEditor::Range match = searcher.search(pattern, range, backwards); QVector result; result.append(match); return result; } //END QWidget *KTextEditor::DocumentPrivate::dialogParent() { QWidget *w = widget(); if (!w) { w = activeView(); if (!w) { w = QApplication::activeWindow(); } } return w; } //BEGIN KTextEditor::HighlightingInterface stuff bool KTextEditor::DocumentPrivate::setMode(const QString &name) { updateFileType(name); return true; } KTextEditor::DefaultStyle KTextEditor::DocumentPrivate::defaultStyleAt(const KTextEditor::Cursor &position) const { // TODO, FIXME KDE5: in surrogate, use 2 bytes before if (! isValidTextPosition(position)) { return dsNormal; } int ds = const_cast(this)-> defStyleNum(position.line(), position.column()); if (ds < 0 || ds > static_cast(dsError)) { return dsNormal; } return static_cast(ds); } QString KTextEditor::DocumentPrivate::mode() const { return m_fileType; } QStringList KTextEditor::DocumentPrivate::modes() const { QStringList m; const QList &modeList = KTextEditor::EditorPrivate::self()->modeManager()->list(); foreach (KateFileType *type, modeList) { m << type->name; } return m; } bool KTextEditor::DocumentPrivate::setHighlightingMode(const QString &name) { int mode = KateHlManager::self()->nameFind(name); if (mode == -1) { return false; } m_buffer->setHighlight(mode); return true; } QString KTextEditor::DocumentPrivate::highlightingMode() const { return highlight()->name(); } QStringList KTextEditor::DocumentPrivate::highlightingModes() const { QStringList hls; for (const auto &hl : KateHlManager::self()->modeList()) { hls << hl.name(); } return hls; } QString KTextEditor::DocumentPrivate::highlightingModeSection(int index) const { return KateHlManager::self()->modeList().at(index).section(); } QString KTextEditor::DocumentPrivate::modeSection(int index) const { return KTextEditor::EditorPrivate::self()->modeManager()->list().at(index)->section; } void KTextEditor::DocumentPrivate::bufferHlChanged() { // update all views makeAttribs(false); // deactivate indenter if necessary m_indenter->checkRequiredStyle(); emit highlightingModeChanged(this); } void KTextEditor::DocumentPrivate::setDontChangeHlOnSave() { m_hlSetByUser = true; } void KTextEditor::DocumentPrivate::bomSetByUser() { m_bomSetByUser = true; } //END //BEGIN KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff void KTextEditor::DocumentPrivate::readSessionConfig(const KConfigGroup &kconfig, const QSet &flags) { - if (!flags.contains(QLatin1String("SkipEncoding"))) { + if (!flags.contains(QStringLiteral("SkipEncoding"))) { // get the encoding QString tmpenc = kconfig.readEntry("Encoding"); if (!tmpenc.isEmpty() && (tmpenc != encoding())) { setEncoding(tmpenc); } } - if (!flags.contains(QLatin1String("SkipUrl"))) { + if (!flags.contains(QStringLiteral("SkipUrl"))) { // restore the url QUrl url(kconfig.readEntry("URL")); // open the file if url valid if (!url.isEmpty() && url.isValid()) { openUrl(url); } else { completed(); //perhaps this should be emitted at the end of this function } } else { completed(); //perhaps this should be emitted at the end of this function } - if (!flags.contains(QLatin1String("SkipMode"))) { + if (!flags.contains(QStringLiteral("SkipMode"))) { // restore the filetype if (kconfig.hasKey("Mode")) { updateFileType(kconfig.readEntry("Mode", fileType())); // restore if set by user, too! m_fileTypeSetByUser = kconfig.readEntry("Mode Set By User", false); } } - if (!flags.contains(QLatin1String("SkipHighlighting"))) { + if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // restore the hl stuff if (kconfig.hasKey("Highlighting")) { const int mode = KateHlManager::self()->nameFind(kconfig.readEntry("Highlighting")); if (mode >= 0) { m_buffer->setHighlight(mode); // restore if set by user, too! see bug 332605, otherwise we loose the hl later again on save m_hlSetByUser = kconfig.readEntry("Highlighting Set By User", false); } } } // indent mode config()->setIndentationMode(kconfig.readEntry("Indentation Mode", config()->indentationMode())); // Restore Bookmarks const QList marks = kconfig.readEntry("Bookmarks", QList()); for (int i = 0; i < marks.count(); i++) { addMark(marks.at(i), KTextEditor::DocumentPrivate::markType01); } } void KTextEditor::DocumentPrivate::writeSessionConfig(KConfigGroup &kconfig, const QSet &flags) { if (this->url().isLocalFile()) { const QString path = this->url().toLocalFile(); if (path.startsWith(QDir::tempPath())) { return; // inside tmp resource, do not save } } - if (!flags.contains(QLatin1String("SkipUrl"))) { + if (!flags.contains(QStringLiteral("SkipUrl"))) { // save url kconfig.writeEntry("URL", this->url().toString()); } - if (!flags.contains(QLatin1String("SkipEncoding"))) { + if (!flags.contains(QStringLiteral("SkipEncoding"))) { // save encoding kconfig.writeEntry("Encoding", encoding()); } - if (!flags.contains(QLatin1String("SkipMode"))) { + if (!flags.contains(QStringLiteral("SkipMode"))) { // save file type kconfig.writeEntry("Mode", m_fileType); // save if set by user, too! kconfig.writeEntry("Mode Set By User", m_fileTypeSetByUser); } - if (!flags.contains(QLatin1String("SkipHighlighting"))) { + if (!flags.contains(QStringLiteral("SkipHighlighting"))) { // save hl kconfig.writeEntry("Highlighting", highlight()->name()); // save if set by user, too! see bug 332605, otherwise we loose the hl later again on save kconfig.writeEntry("Highlighting Set By User", m_hlSetByUser); } // indent mode kconfig.writeEntry("Indentation Mode", config()->indentationMode()); // Save Bookmarks QList marks; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) if (i.value()->type & KTextEditor::MarkInterface::markType01) { marks << i.value()->line; } kconfig.writeEntry("Bookmarks", marks); } //END KTextEditor::SessionConfigInterface and KTextEditor::ParameterizedSessionConfigInterface stuff uint KTextEditor::DocumentPrivate::mark(int line) { KTextEditor::Mark *m = m_marks.value(line); if (!m) { return 0; } return m->type; } void KTextEditor::DocumentPrivate::setMark(int line, uint markType) { clearMark(line); addMark(line, markType); } void KTextEditor::DocumentPrivate::clearMark(int line) { if (line < 0 || line > lastLine()) { return; } if (!m_marks.value(line)) { return; } KTextEditor::Mark *mark = m_marks.take(line); emit markChanged(this, *mark, MarkRemoved); emit marksChanged(this); delete mark; tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::addMark(int line, uint markType) { KTextEditor::Mark *mark; if (line < 0 || line > lastLine()) { return; } if (markType == 0) { return; } if ((mark = m_marks.value(line))) { // Remove bits already set markType &= ~mark->type; if (markType == 0) { return; } // Add bits mark->type |= markType; } else { mark = new KTextEditor::Mark; mark->line = line; mark->type = markType; m_marks.insert(line, mark); } // Emit with a mark having only the types added. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkAdded); emit marksChanged(this); tagLines(line, line); repaintViews(true); } void KTextEditor::DocumentPrivate::removeMark(int line, uint markType) { if (line < 0 || line > lastLine()) { return; } KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } // Remove bits not set markType &= mark->type; if (markType == 0) { return; } // Subtract bits mark->type &= ~markType; // Emit with a mark having only the types removed. KTextEditor::Mark temp; temp.line = line; temp.type = markType; emit markChanged(this, temp, MarkRemoved); if (mark->type == 0) { m_marks.remove(line); delete mark; } emit marksChanged(this); tagLines(line, line); repaintViews(true); } const QHash &KTextEditor::DocumentPrivate::marks() { return m_marks; } void KTextEditor::DocumentPrivate::requestMarkTooltip(int line, QPoint position) { KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { return; } bool handled = false; emit markToolTipRequested(this, *mark, position, handled); } bool KTextEditor::DocumentPrivate::handleMarkClick(int line) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markClicked(this, KTextEditor::Mark{line, 0}, handled); } else { emit markClicked(this, *mark, handled); } return handled; } bool KTextEditor::DocumentPrivate::handleMarkContextMenu(int line, QPoint position) { bool handled = false; KTextEditor::Mark *mark = m_marks.value(line); if (!mark) { emit markContextMenuRequested(this, KTextEditor::Mark{line, 0}, position, handled); } else { emit markContextMenuRequested(this, *mark, position, handled); } return handled; } void KTextEditor::DocumentPrivate::clearMarks() { while (!m_marks.isEmpty()) { QHash::iterator it = m_marks.begin(); KTextEditor::Mark mark = *it.value(); delete it.value(); m_marks.erase(it); emit markChanged(this, mark, MarkRemoved); tagLines(mark.line, mark.line); } m_marks.clear(); emit marksChanged(this); repaintViews(true); } void KTextEditor::DocumentPrivate::setMarkPixmap(MarkInterface::MarkTypes type, const QPixmap &pixmap) { m_markPixmaps.insert(type, pixmap); } void KTextEditor::DocumentPrivate::setMarkDescription(MarkInterface::MarkTypes type, const QString &description) { m_markDescriptions.insert(type, description); } QPixmap KTextEditor::DocumentPrivate::markPixmap(MarkInterface::MarkTypes type) const { return m_markPixmaps.value(type, QPixmap()); } QColor KTextEditor::DocumentPrivate::markColor(MarkInterface::MarkTypes type) const { uint reserved = (0x1 << KTextEditor::MarkInterface::reservedMarkersCount()) - 1; if ((uint)type >= (uint)markType01 && (uint)type <= reserved) { return KateRendererConfig::global()->lineMarkerColor(type); } else { return QColor(); } } QString KTextEditor::DocumentPrivate::markDescription(MarkInterface::MarkTypes type) const { return m_markDescriptions.value(type, QString()); } void KTextEditor::DocumentPrivate::setEditableMarks(uint markMask) { m_editableMarks = markMask; } uint KTextEditor::DocumentPrivate::editableMarks() const { return m_editableMarks; } //END //BEGIN KTextEditor::PrintInterface stuff bool KTextEditor::DocumentPrivate::print() { return KatePrinter::print(this); } void KTextEditor::DocumentPrivate::printPreview() { KatePrinter::printPreview(this); } //END KTextEditor::PrintInterface stuff //BEGIN KTextEditor::DocumentInfoInterface (### unfinished) QString KTextEditor::DocumentPrivate::mimeType() { /** * collect first 4k of text * only heuristic */ QByteArray buf; for (int i = 0; (i < lines()) && (buf.size() <= 4096); ++i) { buf.append(line(i).toUtf8()); buf.append('\n'); } // use path of url, too, if set if (!url().path().isEmpty()) { return QMimeDatabase().mimeTypeForFileNameAndData(url().path(), buf).name(); } // else only use the content return QMimeDatabase().mimeTypeForData(buf).name(); } //END KTextEditor::DocumentInfoInterface //BEGIN: error void KTextEditor::DocumentPrivate::showAndSetOpeningErrorAccess() { QPointer message = new KTextEditor::Message(i18n("The file %1 could not be loaded, as it was not possible to read from it.
Check if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)), KTextEditor::Message::Error); message->setWordWrap(true); QAction *tryAgainAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("translators: you can also translate 'Try Again' with 'Reload'", "Try Again"), nullptr); connect(tryAgainAction, SIGNAL(triggered()), SLOT(documentReload()), Qt::QueuedConnection); QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); // add try again and close actions message->addAction(tryAgainAction); message->addAction(closeAction); // finally post message postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 could not be loaded, as it was not possible to read from it.\n\nCheck if you have read access to this file.", this->url().toDisplayString(QUrl::PreferLocalFile)); } //END: error void KTextEditor::DocumentPrivate::openWithLineLengthLimitOverride() { // raise line length limit to the next power of 2 const int longestLine = m_buffer->longestLineLoaded(); int newLimit = pow(2, ceil(log2(longestLine))); if (newLimit <= longestLine) { newLimit *= 2; } // do the raise config()->setLineLengthLimit(newLimit); // just reload m_buffer->clear(); openFile(); if (!m_openingError) { setReadWrite(true); m_readWriteStateBeforeLoading = true; } } int KTextEditor::DocumentPrivate::lineLengthLimit() const { return config()->lineLengthLimit(); } //BEGIN KParts::ReadWrite stuff bool KTextEditor::DocumentPrivate::openFile() { /** * we are about to invalidate all cursors/ranges/.. => m_buffer->openFile will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // no open errors until now... m_openingError = false; m_openingErrorMessage.clear (); // add new m_file to dirwatch activateDirWatch(); // remember current encoding QString currentEncoding = encoding(); // // mime type magic to get encoding right // QString mimeType = arguments().mimeType(); int pos = mimeType.indexOf(QLatin1Char(';')); if (pos != -1 && !(m_reloading && m_userSetEncodingForNextReload)) { setEncoding(mimeType.mid(pos + 1)); } // update file type, we do this here PRE-LOAD, therefore pass file name for reading from updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, localFilePath())); // read dir config (if possible and wanted) // do this PRE-LOAD to get encoding info! readDirConfig(); // perhaps we need to re-set again the user encoding if (m_reloading && m_userSetEncodingForNextReload && (currentEncoding != encoding())) { setEncoding(currentEncoding); } bool success = m_buffer->openFile(localFilePath(), (m_reloading && m_userSetEncodingForNextReload)); // // yeah, success // read variables // if (success) { readVariables(); } // // update views // foreach (KTextEditor::ViewPrivate *view, m_views) { // This is needed here because inserting the text moves the view's start position (it is a MovingCursor) view->setCursorPosition(KTextEditor::Cursor()); view->updateView(true); } // Inform that the text has changed (required as we're not inside the usual editStart/End stuff) emit textChanged(this); emit loaded(this); // // to houston, we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // // display errors // if (!success) { showAndSetOpeningErrorAccess(); } // warn: broken encoding if (m_buffer->brokenEncoding()) { // this file can't be saved again without killing it setReadWrite(false); m_readWriteStateBeforeLoading=false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened with %2 encoding but contained invalid characters.
" "It is set to read-only mode, as saving might destroy its content.
" "Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())), KTextEditor::Message::Warning); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 was opened with %2 encoding but contained invalid characters." " It is set to read-only mode, as saving might destroy its content." " Either reopen the file with the correct encoding chosen or enable the read-write mode again in the tools menu to be able to edit it.", this->url().toDisplayString(QUrl::PreferLocalFile), QString::fromLatin1(m_buffer->textCodec()->name())); } // warn: too long lines if (m_buffer->tooLongLinesWrapped()) { // this file can't be saved again without modifications setReadWrite(false); m_readWriteStateBeforeLoading = false; QPointer message = new KTextEditor::Message(i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(), m_buffer->longestLineLoaded()), KTextEditor::Message::Warning); QAction *increaseAndReload = new QAction(i18n("Temporarily raise limit and reload file"), message); connect(increaseAndReload, SIGNAL(triggered()), this, SLOT(openWithLineLengthLimitOverride())); message->addAction(increaseAndReload, true); message->addAction(new QAction(i18n("Close"), message), true); message->setWordWrap(true); postMessage(message); // remember error m_openingError = true; m_openingErrorMessage = i18n("The file %1 was opened and contained lines longer than the configured Line Length Limit (%2 characters).
" "The longest of those lines was %3 characters long
" "Those lines were wrapped and the document is set to read-only mode, as saving will modify its content.", this->url().toDisplayString(QUrl::PreferLocalFile), config()->lineLengthLimit(),m_buffer->longestLineLoaded()); } // // return the success // return success; } bool KTextEditor::DocumentPrivate::saveFile() { // delete pending mod-on-hd message if applicable. delete m_modOnHdHandler; // some warnings, if file was changed by the outside! if (!url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { QString str = reasonedMOHString() + QLatin1String("\n\n"); if (!isModified()) { if (KMessageBox::warningContinueCancel(dialogParent(), str + i18n("Do you really want to save this unmodified file? You could overwrite changed data in the file on disk."), i18n("Trying to Save Unmodified File"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } else { if (KMessageBox::warningContinueCancel(dialogParent(), str + i18n("Do you really want to save this file? Both your open file and the file on disk were changed. There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue) { return false; } } } } // // can we encode it if we want to save it ? // if (!m_buffer->canEncode() && (KMessageBox::warningContinueCancel(dialogParent(), i18n("The selected encoding cannot encode every Unicode character in this document. Do you really want to save it? There could be some data lost."), i18n("Possible Data Loss"), KGuiItem(i18n("Save Nevertheless"))) != KMessageBox::Continue)) { return false; } /** * create a backup file or abort if that fails! * if no backup file wanted, this routine will just return true */ if (!createBackupFile()) return false; // update file type, pass no file path, read file type content from this document QString oldPath = m_dirWatchFile; // only update file type if path has changed so that variables are not overridden on normal save if (oldPath != localFilePath()) { updateFileType(KTextEditor::EditorPrivate::self()->modeManager()->fileType(this, QString())); if (url().isLocalFile()) { // if file is local then read dir config for new path readDirConfig(); } } // read our vars readVariables(); // remove file from dirwatch deactivateDirWatch(); // remove all trailing spaces in the document (as edit actions) // NOTE: we need this as edit actions, since otherwise the edit actions // in the swap file recovery may happen at invalid cursor positions removeTrailingSpaces(); // // try to save // if (!m_buffer->saveFile(localFilePath())) { // add m_file again to dirwatch activateDirWatch(oldPath); KMessageBox::error(dialogParent(), i18n("The document could not be saved, as it was not possible to write to %1.\nCheck that you have write access to this file or that enough disk space is available.\nThe original file may be lost or damaged. Don't quit the application until the file is successfully written.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // update the checksum createDigest(); // add m_file again to dirwatch activateDirWatch(); // // we are not modified // if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // (dominik) mark last undo group as not mergeable, otherwise the next // edit action might be merged and undo will never stop at the saved state m_undoManager->undoSafePoint(); m_undoManager->updateLineModifications(); // // return success // return true; } bool KTextEditor::DocumentPrivate::createBackupFile() { /** * backup for local or remote files wanted? */ const bool backupLocalFiles = config()->backupOnSaveLocal(); const bool backupRemoteFiles = config()->backupOnSaveRemote(); /** * early out, before mount check: backup wanted at all? * => if not, all fine, just return */ if (!backupLocalFiles && !backupRemoteFiles) { return true; } /** * decide if we need backup based on locality * skip that, if we always want backups, as currentMountPoints is not that fast */ QUrl u(url()); bool needBackup = backupLocalFiles && backupRemoteFiles; if (!needBackup) { bool slowOrRemoteFile = !u.isLocalFile(); if (!slowOrRemoteFile) { // could be a mounted remote filesystem (e.g. nfs, sshfs, cifs) // we have the early out above to skip this, if we want no backup, which is the default KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByDevice(u.toLocalFile()); slowOrRemoteFile = (mountPoint && mountPoint->probablySlow()); } needBackup = (!slowOrRemoteFile && backupLocalFiles) || (slowOrRemoteFile && backupRemoteFiles); } /** * no backup needed? be done */ if (!needBackup) { return true; } /** * else: try to backup */ if (config()->backupPrefix().contains(QDir::separator())) { /** * replace complete path, as prefix is a path! */ u.setPath(config()->backupPrefix() + u.fileName() + config()->backupSuffix()); } else { /** * replace filename in url */ const QString fileName = u.fileName(); u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + config()->backupPrefix() + fileName + config()->backupSuffix()); } qCDebug(LOG_KTE) << "backup src file name: " << url(); qCDebug(LOG_KTE) << "backup dst file name: " << u; // handle the backup... bool backupSuccess = false; // local file mode, no kio if (u.isLocalFile()) { if (QFile::exists(url().toLocalFile())) { // first: check if backupFile is already there, if true, unlink it QFile backupFile(u.toLocalFile()); if (backupFile.exists()) { backupFile.remove(); } backupSuccess = QFile::copy(url().toLocalFile(), u.toLocalFile()); } else { backupSuccess = true; } } else { // remote file mode, kio // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); if (statJob->exec()) { // do a evil copy which will overwrite target if possible KFileItem item(statJob->statResult(), url()); KIO::FileCopyJob *job = KIO::file_copy(url(), u, item.permissions(), KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); backupSuccess = job->exec(); } else { backupSuccess = true; } } // backup has failed, ask user how to proceed if (!backupSuccess && (KMessageBox::warningContinueCancel(dialogParent() , i18n("For file %1 no backup copy could be created before saving." " If an error occurs while saving, you might lose the data of this file." " A reason could be that the media you write to is full or the directory of the file is read-only for you.", url().toDisplayString(QUrl::PreferLocalFile)) , i18n("Failed to create backup copy.") , KGuiItem(i18n("Try to Save Nevertheless")) , KStandardGuiItem::cancel(), QStringLiteral("Backup Failed Warning")) != KMessageBox::Continue)) { return false; } return true; } void KTextEditor::DocumentPrivate::readDirConfig() { if (!url().isLocalFile()) { return; } /** * first search .kateconfig upwards * with recursion guard */ QSet seenDirectories; QDir dir (QFileInfo(localFilePath()).absolutePath()); while (!seenDirectories.contains (dir.absolutePath ())) { /** * fill recursion guard */ seenDirectories.insert (dir.absolutePath ()); // try to open config file in this dir QFile f(dir.absolutePath () + QLatin1String("/.kateconfig")); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); uint linesRead = 0; QString line = stream.readLine(); while ((linesRead < 32) && !line.isNull()) { readVariableLine(line); line = stream.readLine(); linesRead++; } return; } /** * else: cd up, if possible or abort */ if (!dir.cdUp()) { break; } } #if EDITORCONFIG_FOUND // if there wasn’t any .kateconfig file and KTextEditor was compiled with // EditorConfig support, try to load document config from a .editorconfig // file, if such is provided EditorConfig editorConfig(this); editorConfig.parse(); #endif } void KTextEditor::DocumentPrivate::activateDirWatch(const QString &useFileName) { QString fileToUse = useFileName; if (fileToUse.isEmpty()) { fileToUse = localFilePath(); } QFileInfo fileInfo = QFileInfo(fileToUse); if (fileInfo.isSymLink()) { // Monitor the actual data and not the symlink fileToUse = fileInfo.canonicalFilePath(); } // same file as we are monitoring, return if (fileToUse == m_dirWatchFile) { return; } // remove the old watched file deactivateDirWatch(); // add new file if needed if (url().isLocalFile() && !fileToUse.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->addFile(fileToUse); m_dirWatchFile = fileToUse; } } void KTextEditor::DocumentPrivate::deactivateDirWatch() { if (!m_dirWatchFile.isEmpty()) { KTextEditor::EditorPrivate::self()->dirWatch()->removeFile(m_dirWatchFile); } m_dirWatchFile.clear(); } bool KTextEditor::DocumentPrivate::openUrl(const QUrl &url) { if (!m_reloading) { // Reset filetype when opening url m_fileTypeSetByUser = false; } bool res = KTextEditor::Document::openUrl(normalizeUrl(url)); updateDocName(); return res; } bool KTextEditor::DocumentPrivate::closeUrl() { // // file mod on hd // if (!m_reloading && !url().isEmpty()) { if (m_fileChangedDialogsActivated && m_modOnHd) { // make sure to not forget pending mod-on-hd handler delete m_modOnHdHandler; QWidget *parentWidget(dialogParent()); if (!(KMessageBox::warningContinueCancel( parentWidget, reasonedMOHString() + QLatin1String("\n\n") + i18n("Do you really want to continue to close this file? Data loss may occur."), i18n("Possible Data Loss"), KGuiItem(i18n("Close Nevertheless")), KStandardGuiItem::cancel(), QStringLiteral("kate_close_modonhd_%1").arg(m_modOnHdReason)) == KMessageBox::Continue)) { /** * reset reloading */ m_reloading = false; return false; } } } // // first call the normal kparts implementation // if (!KParts::ReadWritePart::closeUrl()) { /** * reset reloading */ m_reloading = false; return false; } // Tell the world that we're about to go ahead with the close if (!m_reloading) { emit aboutToClose(this); } /** * delete all KTE::Messages */ if (!m_messageHash.isEmpty()) { QList keys = m_messageHash.keys(); foreach (KTextEditor::Message *message, keys) { delete message; } } /** * we are about to invalidate all cursors/ranges/.. => m_buffer->clear will do so */ emit aboutToInvalidateMovingInterfaceContent(this); // remove file from dirwatch deactivateDirWatch(); // // empty url + fileName // setUrl(QUrl()); setLocalFilePath(QString()); // we are not modified if (m_modOnHd) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } // remove all marks clearMarks(); // clear the buffer m_buffer->clear(); // clear undo/redo history m_undoManager->clearUndo(); m_undoManager->clearRedo(); // no, we are no longer modified setModified(false); // we have no longer any hl m_buffer->setHighlight(0); // update all our views foreach (KTextEditor::ViewPrivate *view, m_views) { view->clearSelection(); // fix bug #118588 view->clear(); } // purge swap file if (m_swapfile) { m_swapfile->fileClosed(); } // success return true; } bool KTextEditor::DocumentPrivate::isDataRecoveryAvailable() const { return m_swapfile && m_swapfile->shouldRecover(); } void KTextEditor::DocumentPrivate::recoverData() { if (isDataRecoveryAvailable()) { m_swapfile->recover(); } } void KTextEditor::DocumentPrivate::discardDataRecovery() { if (isDataRecoveryAvailable()) { m_swapfile->discard(); } } void KTextEditor::DocumentPrivate::setReadWrite(bool rw) { if (isReadWrite() == rw) { return; } KParts::ReadWritePart::setReadWrite(rw); foreach (KTextEditor::ViewPrivate *view, m_views) { view->slotUpdateUndo(); view->slotReadWriteChanged(); } emit readWriteChanged(this); } void KTextEditor::DocumentPrivate::setModified(bool m) { if (isModified() != m) { KParts::ReadWritePart::setModified(m); foreach (KTextEditor::ViewPrivate *view, m_views) { view->slotUpdateUndo(); } emit modifiedChanged(this); } m_undoManager->setModified(m); } //END //BEGIN Kate specific stuff ;) void KTextEditor::DocumentPrivate::makeAttribs(bool needInvalidate) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->renderer()->updateAttributes(); } if (needInvalidate) { m_buffer->invalidateHighlighting(); } foreach (KTextEditor::ViewPrivate *view, m_views) { view->tagAll(); view->updateView(true); } } // the attributes of a hl have changed, update void KTextEditor::DocumentPrivate::internalHlChanged() { makeAttribs(); } void KTextEditor::DocumentPrivate::addView(KTextEditor::View *view) { Q_ASSERT (!m_views.contains(view)); m_views.insert(view, static_cast(view)); m_viewsCache.append(view); // apply the view & renderer vars from the file type if (!m_fileType.isEmpty()) { readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(m_fileType).varLine, true); } // apply the view & renderer vars from the file readVariables(true); setActiveView(view); } void KTextEditor::DocumentPrivate::removeView(KTextEditor::View *view) { Q_ASSERT (m_views.contains(view)); m_views.remove(view); m_viewsCache.removeAll(view); if (activeView() == view) { setActiveView(nullptr); } } void KTextEditor::DocumentPrivate::setActiveView(KTextEditor::View *view) { if (m_activeView == view) { return; } m_activeView = static_cast(view); } bool KTextEditor::DocumentPrivate::ownedView(KTextEditor::ViewPrivate *view) { // do we own the given view? return (m_views.contains(view)); } int KTextEditor::DocumentPrivate::toVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->toVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::toVirtualColumn(const KTextEditor::Cursor &cursor) const { return toVirtualColumn(cursor.line(), cursor.column()); } int KTextEditor::DocumentPrivate::fromVirtualColumn(int line, int column) const { Kate::TextLine textLine = m_buffer->plainLine(line); if (textLine) { return textLine->fromVirtualColumn(column, config()->tabWidth()); } else { return 0; } } int KTextEditor::DocumentPrivate::fromVirtualColumn(const KTextEditor::Cursor &cursor) const { return fromVirtualColumn(cursor.line(), cursor.column()); } void KTextEditor::DocumentPrivate::typeChars(KTextEditor::ViewPrivate *view, QString chars) { // nop for empty chars if (chars.isEmpty()) { return; } // auto bracket handling QChar closingBracket; if (view->config()->autoBrackets()) { // Check if entered closing bracket is already balanced const QChar typedChar = chars.at(0); const QChar openBracket = matchingStartBracket(typedChar); if (!openBracket.isNull()) { KTextEditor::Cursor curPos = view->cursorPosition(); if ((characterAt(curPos) == typedChar) && findMatchingBracket(curPos, 123/*Which value may best?*/).isValid()) { // Do nothing view->cursorRight(); return; } } // for newly inserted text: remember if we should auto add some bracket if (chars.size() == 1) { // we inserted a bracket? => remember the matching closing one closingBracket = matchingEndBracket(typedChar); // closing bracket for the autobracket we inserted earlier? if (m_currentAutobraceClosingChar == typedChar && m_currentAutobraceRange) { // do nothing m_currentAutobraceRange.reset(nullptr); view->cursorRight(); return; } } } // Treat some char also as "auto bracket" only when we have a selection if (view->selection() && closingBracket.isNull() && view->config()->encloseSelectionInChars()) { const QChar typedChar = chars.at(0); if (view->config()->charsToEncloseSelection().contains(typedChar)) { // The unconditional mirroring cause no harm, but allows funny brackets closingBracket = typedChar.mirroredChar(); } } editStart(); /** * special handling if we want to add auto brackets to a selection */ if (view->selection() && !closingBracket.isNull()) { std::unique_ptr selectionRange(newMovingRange(view->selectionRange())); const int startLine = qMax(0, selectionRange->start().line()); const int endLine = qMin(selectionRange->end().line(), lastLine()); const bool blockMode = view->blockSelection() && (startLine != endLine); if (blockMode) { if (selectionRange->start().column() > selectionRange->end().column()) { // Selection was done from right->left, requires special setting to ensure the new // added brackets will not be part of the selection selectionRange->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight); } // Add brackets to each line of the block const int startColumn = qMin(selectionRange->start().column(), selectionRange->end().column()); const int endColumn = qMax(selectionRange->start().column(), selectionRange->end().column()); const KTextEditor::Range workingRange(startLine, startColumn, endLine, endColumn); for (int line = startLine; line <= endLine; ++line) { const KTextEditor::Range r(rangeOnLine(workingRange, line)); insertText(r.end(), QString(closingBracket)); view->slotTextInserted(view, r.end(), QString(closingBracket)); insertText(r.start(), chars); view->slotTextInserted(view, r.start(), chars); } } else { // No block, just add to start & end of selection insertText(selectionRange->end(), QString(closingBracket)); view->slotTextInserted(view, selectionRange->end(), QString(closingBracket)); insertText(selectionRange->start(), chars); view->slotTextInserted(view, selectionRange->start(), chars); } // Refesh selection view->setSelection(selectionRange->toRange()); view->setCursorPosition(selectionRange->end()); editEnd(); return; } /** * normal handling */ if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } const KTextEditor::Cursor oldCur(view->cursorPosition()); const bool multiLineBlockMode = view->blockSelection() && view->selection(); if (view->currentInputMode()->overwrite()) { // blockmode multiline selection case: remove chars in every line const KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = multiLineBlockMode ? qMax(0, selectionRange.start().line()) : view->cursorPosition().line(); const int endLine = multiLineBlockMode ? qMin(selectionRange.end().line(), lastLine()) : startLine; const int virtualColumn = toVirtualColumn(multiLineBlockMode ? selectionRange.end() : view->cursorPosition()); for (int line = endLine; line >= startLine; --line) { Kate::TextLine textLine = m_buffer->plainLine(line); Q_ASSERT(textLine); const int column = fromVirtualColumn(line, virtualColumn); KTextEditor::Range r = KTextEditor::Range(KTextEditor::Cursor(line, column), qMin(chars.length(), textLine->length() - column)); // replace mode needs to know what was removed so it can be restored with backspace if (oldCur.column() < lineLength(line)) { QChar removed = characterAt(KTextEditor::Cursor(line, column)); view->currentInputMode()->overwrittenChar(removed); } removeText(r); } } chars = eventuallyReplaceTabs(view->cursorPosition(), chars); if (multiLineBlockMode) { KTextEditor::Range selectionRange = view->selectionRange(); const int startLine = qMax(0, selectionRange.start().line()); const int endLine = qMin(selectionRange.end().line(), lastLine()); const int column = toVirtualColumn(selectionRange.end()); for (int line = endLine; line >= startLine; --line) { editInsertText(line, fromVirtualColumn(line, column), chars); } int newSelectionColumn = toVirtualColumn(view->cursorPosition()); selectionRange.setRange(KTextEditor::Cursor(selectionRange.start().line(), fromVirtualColumn(selectionRange.start().line(), newSelectionColumn)) , KTextEditor::Cursor(selectionRange.end().line(), fromVirtualColumn(selectionRange.end().line(), newSelectionColumn))); view->setSelection(selectionRange); } else { insertText(view->cursorPosition(), chars); } /** * auto bracket handling for newly inserted text * we inserted a bracket? * => add the matching closing one to the view + input chars * try to preserve the cursor position */ bool skipAutobrace = closingBracket == QLatin1Char('\''); if (highlight() && skipAutobrace) { // skip adding ' in spellchecked areas, because those are text skipAutobrace = highlight()->spellCheckingRequiredForLocation(this, view->cursorPosition() - Cursor{0, 1}); } const auto cursorPos(view->cursorPosition()); if (!skipAutobrace && (closingBracket == QLatin1Char('\''))) { // skip auto quotes when these looks already balanced, bug 405089 Kate::TextLine textLine = m_buffer->plainLine(cursorPos.line()); // RegEx match quote, but not excaped quote, thanks to https://stackoverflow.com/a/11819111 const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?plainLine(cursorPos.line()); const int count = textLine->text().left(cursorPos.column()).count(QRegularExpression(QStringLiteral("(?document()->text({cursorPos, cursorPos + Cursor{0, 1}}).trimmed(); if (nextChar.isEmpty() || !nextChar.at(0).isLetterOrNumber()) { insertText(view->cursorPosition(), QString(closingBracket)); const auto insertedAt(view->cursorPosition()); view->setCursorPosition(cursorPos); m_currentAutobraceRange.reset(newMovingRange({cursorPos - Cursor{0, 1}, insertedAt}, KTextEditor::MovingRange::DoNotExpand)); connect(view, &View::cursorPositionChanged, this, &DocumentPrivate::checkCursorForAutobrace, Qt::UniqueConnection); // add bracket to chars inserted! needed for correct signals + indent chars.append(closingBracket); } m_currentAutobraceClosingChar = closingBracket; } // end edit session here, to have updated HL in userTypedChar! editEnd(); // trigger indentation KTextEditor::Cursor b(view->cursorPosition()); m_indenter->userTypedChar(view, b, chars.isEmpty() ? QChar() : chars.at(chars.length() - 1)); /** * inform the view about the original inserted chars */ view->slotTextInserted(view, oldCur, chars); } void KTextEditor::DocumentPrivate::checkCursorForAutobrace(KTextEditor::View*, const KTextEditor::Cursor& newPos) { if ( m_currentAutobraceRange && ! m_currentAutobraceRange->toRange().contains(newPos) ) { m_currentAutobraceRange.clear(); } } void KTextEditor::DocumentPrivate::newLine(KTextEditor::ViewPrivate *v, KTextEditor::DocumentPrivate::NewLineIndent indent) { editStart(); if (!v->config()->persistentSelection() && v->selection()) { v->removeSelectedText(); v->clearSelection(); } // query cursor position KTextEditor::Cursor c = v->cursorPosition(); if (c.line() > lastLine()) { c.setLine(lastLine()); } if (c.line() < 0) { c.setLine(0); } int ln = c.line(); Kate::TextLine textLine = plainKateTextLine(ln); if (c.column() > textLine->length()) { c.setColumn(textLine->length()); } // first: wrap line editWrapLine(c.line(), c.column()); // end edit session here, to have updated HL in userTypedChar! editEnd(); // second: if "indent" is true, indent the new line, if needed... if (indent == KTextEditor::DocumentPrivate::Indent) { m_indenter->userTypedChar(v, v->cursorPosition(), QLatin1Char('\n')); } } void KTextEditor::DocumentPrivate::transpose(const KTextEditor::Cursor &cursor) { Kate::TextLine textLine = m_buffer->plainLine(cursor.line()); if (!textLine || (textLine->length() < 2)) { return; } uint col = cursor.column(); if (col > 0) { col--; } if ((textLine->length() - col) < 2) { return; } uint line = cursor.line(); QString s; //clever swap code if first character on the line swap right&left //otherwise left & right s.append(textLine->at(col + 1)); s.append(textLine->at(col)); //do the swap // do it right, never ever manipulate a textline editStart(); editRemoveText(line, col, 2); editInsertText(line, col, s); editEnd(); } void KTextEditor::DocumentPrivate::backspace(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && view->selection() && range.start().column() > 0 && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character before vertical selection line by expanding the selection range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } uint col = qMax(c.column(), 0); uint line = qMax(c.line(), 0); if ((col == 0) && (line == 0)) { return; } const Kate::TextLine textLine = m_buffer->plainLine(line); // don't forget this check!!!! really!!!! if (!textLine) { return; } if (col > 0) { bool useNextBlock = false; if (config()->backspaceIndents()) { // backspace indents: erase to next indent position int colX = textLine->toVirtualColumn(col, config()->tabWidth()); int pos = textLine->firstChar(); if (pos > 0) { pos = textLine->toVirtualColumn(pos, config()->tabWidth()); } if (pos < 0 || pos >= (int)colX) { // only spaces on left side of cursor indent(KTextEditor::Range(line, 0, line, 0), -1); } else { useNextBlock = true; } } if (!config()->backspaceIndents() || useNextBlock) { KTextEditor::Cursor beginCursor(line, 0); KTextEditor::Cursor endCursor(line, col); if (!view->config()->backspaceRemoveComposed()) { // Normal backspace behavior beginCursor.setColumn(col - 1); // move to left of surrogate pair if (!isValidTextPosition(beginCursor)) { Q_ASSERT(col >= 2); beginCursor.setColumn(col - 2); } } else { beginCursor.setColumn(view->textLayout(c)->previousCursorPosition(c.column())); } removeText(KTextEditor::Range(beginCursor, endCursor)); // in most cases cursor is moved by removeText, but we should do it manually // for past-end-of-line cursors in block mode view->setCursorPosition(beginCursor); } } else { // col == 0: wrap to previous line const Kate::TextLine textLine = m_buffer->plainLine(line - 1); if (line > 0 && textLine) { - if (config()->wordWrap() && textLine->endsWith(QLatin1String(" "))) { + if (config()->wordWrap() && textLine->endsWith(QStringLiteral(" "))) { // gg: in hard wordwrap mode, backspace must also eat the trailing space removeText(KTextEditor::Range(line - 1, textLine->length() - 1, line, 0)); } else { removeText(KTextEditor::Range(line - 1, textLine->length(), line, 0)); } } } if (m_currentAutobraceRange) { const auto r = m_currentAutobraceRange->toRange(); if (r.columnWidth() == 1 && view->cursorPosition() == r.start()) { // start parenthesis removed and range length is 1, remove end as well del(view, view->cursorPosition()); m_currentAutobraceRange.clear(); } } } void KTextEditor::DocumentPrivate::del(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &c) { if (!view->config()->persistentSelection() && view->selection()) { KTextEditor::Range range = view->selectionRange(); editStart(); // Avoid bad selection in case of undo if (view->blockSelection() && toVirtualColumn(range.start()) == toVirtualColumn(range.end())) { // Remove one character after vertical selection line by expanding the selection range.setEnd(KTextEditor::Cursor(range.end().line(), range.end().column() + 1)); view->setSelection(range); } view->removeSelectedText(); editEnd(); return; } if (c.column() < (int) m_buffer->plainLine(c.line())->length()) { KTextEditor::Cursor endCursor(c.line(), view->textLayout(c)->nextCursorPosition(c.column())); removeText(KTextEditor::Range(c, endCursor)); } else if (c.line() < lastLine()) { removeText(KTextEditor::Range(c.line(), c.column(), c.line() + 1, 0)); } } void KTextEditor::DocumentPrivate::paste(KTextEditor::ViewPrivate *view, const QString &text) { // nop if nothing to paste if (text.isEmpty()) { return; } // normalize line endings, to e.g. catch issues with \r\n in paste buffer // see bug 410951 QString s = text; s.replace(QRegularExpression(QStringLiteral("(\r\n|\r|\n)")), QStringLiteral("\n")); int lines = s.count(QLatin1Char('\n')); m_undoManager->undoSafePoint(); editStart(); KTextEditor::Cursor pos = view->cursorPosition(); if (!view->config()->persistentSelection() && view->selection()) { pos = view->selectionRange().start(); if (view->blockSelection()) { pos = rangeOnLine(view->selectionRange(), pos.line()).start(); if (lines == 0) { s += QLatin1Char('\n'); s = s.repeated(view->selectionRange().numberOfLines() + 1); s.chop(1); } } view->removeSelectedText(); } if (config()->ovr()) { QStringList pasteLines = s.split(QLatin1Char('\n')); if (!view->blockSelection()) { int endColumn = (pasteLines.count() == 1 ? pos.column() : 0) + pasteLines.last().length(); removeText(KTextEditor::Range(pos, pos.line() + pasteLines.count() - 1, endColumn)); } else { int maxi = qMin(pos.line() + pasteLines.count(), this->lines()); for (int i = pos.line(); i < maxi; ++i) { int pasteLength = pasteLines.at(i - pos.line()).length(); removeText(KTextEditor::Range(i, pos.column(), i, qMin(pasteLength + pos.column(), lineLength(i)))); } } } insertText(pos, s, view->blockSelection()); editEnd(); // move cursor right for block select, as the user is moved right internal // even in that case, but user expects other behavior in block selection // mode ! // just let cursor stay, that was it before I changed to moving ranges! if (view->blockSelection()) { view->setCursorPositionInternal(pos); } if (config()->indentPastedText()) { KTextEditor::Range range = KTextEditor::Range(KTextEditor::Cursor(pos.line(), 0), KTextEditor::Cursor(pos.line() + lines, 0)); m_indenter->indent(view, range); } if (!view->blockSelection()) { emit charactersSemiInteractivelyInserted(pos, s); } m_undoManager->undoSafePoint(); } void KTextEditor::DocumentPrivate::indent(KTextEditor::Range range, int change) { if (!isReadWrite()) { return; } editStart(); m_indenter->changeIndent(range, change); editEnd(); } void KTextEditor::DocumentPrivate::align(KTextEditor::ViewPrivate *view, const KTextEditor::Range &range) { m_indenter->indent(view, range); } void KTextEditor::DocumentPrivate::insertTab(KTextEditor::ViewPrivate *view, const KTextEditor::Cursor &) { if (!isReadWrite()) { return; } int lineLen = line(view->cursorPosition().line()).length(); KTextEditor::Cursor c = view->cursorPosition(); editStart(); if (!view->config()->persistentSelection() && view->selection()) { view->removeSelectedText(); } else if (view->currentInputMode()->overwrite() && c.column() < lineLen) { KTextEditor::Range r = KTextEditor::Range(view->cursorPosition(), 1); // replace mode needs to know what was removed so it can be restored with backspace QChar removed = line(view->cursorPosition().line()).at(r.start().column()); view->currentInputMode()->overwrittenChar(removed); removeText(r); } c = view->cursorPosition(); editInsertText(c.line(), c.column(), QStringLiteral("\t")); editEnd(); } /* Remove a given string at the beginning of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromBeginning(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->startsWith(str); if (!there) { cursor.setColumn(textline->firstChar()); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Remove a given string at the end of the current line. */ bool KTextEditor::DocumentPrivate::removeStringFromEnd(int line, const QString &str) { Kate::TextLine textline = m_buffer->plainLine(line); KTextEditor::Cursor cursor(line, 0); bool there = textline->endsWith(str); if (there) { cursor.setColumn(textline->length() - str.length()); } else { cursor.setColumn(textline->lastChar() - str.length() + 1); there = textline->matchesAt(cursor.column(), str); } if (there) { // Remove some chars removeText(KTextEditor::Range(cursor, str.length())); } return there; } /* Replace tabs by spaces in the given string, if enabled. */ QString KTextEditor::DocumentPrivate::eventuallyReplaceTabs(const KTextEditor::Cursor &cursorPos, const QString &str) const { const bool replacetabs = config()->replaceTabsDyn(); if ( ! replacetabs ) { return str; } const int indentWidth = config()->indentationWidth(); static const QLatin1Char tabChar('\t'); int column = cursorPos.column(); // The result will always be at least as long as the input QString result; result.reserve(str.size()); Q_FOREACH (const QChar ch, str) { if (ch == tabChar) { // Insert only enough spaces to align to the next indentWidth column // This fixes bug #340212 int spacesToInsert = indentWidth - (column % indentWidth); result += QStringLiteral(" ").repeated(spacesToInsert); column += spacesToInsert; } else { // Just keep all other typed characters as-is result += ch; ++column; } } return result; } /* Add to the current line a comment line mark at the beginning. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSingleLine(int line, int attrib) { QString commentLineMark = highlight()->getCommentSingleLineStart(attrib); int pos = -1; if (highlight()->getCommentSingleLinePosition(attrib) == KSyntaxHighlighting::CommentPosition::StartOfLine) { pos = 0; commentLineMark += QLatin1Char(' '); } else { const Kate::TextLine l = kateTextLine(line); pos = l->firstChar(); } if (pos >= 0) { insertText(KTextEditor::Cursor(line, pos), commentLineMark); } } /* Remove from the current line a comment line mark at the beginning if there is one. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSingleLine(int line, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); editStart(); // Try to remove the long comment mark first bool removed = (removeStringFromBeginning(line, longCommentMark) || removeStringFromBeginning(line, shortCommentMark)); editEnd(); return removed; } /* Add to the current line a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSingleLine(int line, int attrib) { const QString startCommentMark = highlight()->getCommentStart(attrib) + QLatin1Char(' '); const QString stopCommentMark = QLatin1Char(' ') + highlight()->getCommentEnd(attrib); editStart(); // Add the start comment mark insertText(KTextEditor::Cursor(line, 0), startCommentMark); // Go to the end of the line const int col = m_buffer->plainLine(line)->length(); // Add the stop comment mark insertText(KTextEditor::Cursor(line, col), stopCommentMark); editEnd(); } /* Remove from the current line a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSingleLine(int line, int attrib) { const QString shortStartCommentMark = highlight()->getCommentStart(attrib); const QString longStartCommentMark = shortStartCommentMark + QLatin1Char(' '); const QString shortStopCommentMark = highlight()->getCommentEnd(attrib); const QString longStopCommentMark = QLatin1Char(' ') + shortStopCommentMark; editStart(); // Try to remove the long start comment mark first const bool removedStart = (removeStringFromBeginning(line, longStartCommentMark) || removeStringFromBeginning(line, shortStartCommentMark)); // Try to remove the long stop comment mark first const bool removedStop = removedStart && (removeStringFromEnd(line, longStopCommentMark) || removeStringFromEnd(line, shortStopCommentMark)); editEnd(); return (removedStart || removedStop); } /* Add to the current selection a start comment mark at the beginning and a stop comment mark at the end. */ void KTextEditor::DocumentPrivate::addStartStopCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); KTextEditor::Range range = view->selectionRange(); if ((range.end().column() == 0) && (range.end().line() > 0)) { range.setEnd(KTextEditor::Cursor(range.end().line() - 1, lineLength(range.end().line() - 1))); } editStart(); if (!view->blockSelection()) { insertText(range.end(), endComment); insertText(range.start(), startComment); } else { for (int line = range.start().line(); line <= range.end().line(); line++) { KTextEditor::Range subRange = rangeOnLine(range, line); insertText(subRange.end(), endComment); insertText(subRange.start(), startComment); } } editEnd(); // selection automatically updated (MovingRange) } /* Add to the current selection a comment line mark at the beginning of each line. */ void KTextEditor::DocumentPrivate::addStartLineCommentToSelection(KTextEditor::ViewPrivate *view, int attrib) { //const QString commentLineMark = highlight()->getCommentSingleLineStart(attrib) + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); // if end of selection is in column 0 in last line, omit the last line if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { //insertText (z, 0, commentLineMark); addStartLineCommentToSingleLine(z, attrib); } editEnd(); // selection automatically updated (MovingRange) } bool KTextEditor::DocumentPrivate::nextNonSpaceCharPos(int &line, int &col) { for (; line < (int)m_buffer->count(); line++) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->nextNonSpaceChar(col); if (col != -1) { return true; // Next non-space char found } col = 0; } // No non-space char found line = -1; col = -1; return false; } bool KTextEditor::DocumentPrivate::previousNonSpaceCharPos(int &line, int &col) { while (true) { Kate::TextLine textLine = m_buffer->plainLine(line); if (!textLine) { break; } col = textLine->previousNonSpaceChar(col); if (col != -1) { return true; } if (line == 0) { return false; } --line; col = textLine->length(); } // No non-space char found line = -1; col = -1; return false; } /* Remove from the selection a start comment mark at the beginning and a stop comment mark at the end. */ bool KTextEditor::DocumentPrivate::removeStartStopCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); int sl = qMax (0, view->selectionRange().start().line()); int el = qMin (view->selectionRange().end().line(), lastLine()); int sc = view->selectionRange().start().column(); int ec = view->selectionRange().end().column(); // The selection ends on the char before selectEnd if (ec != 0) { --ec; } else if (el > 0) { --el; ec = m_buffer->plainLine(el)->length() - 1; } const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); // had this been perl or sed: s/^\s*$startComment(.+?)$endComment\s*/$2/ bool remove = nextNonSpaceCharPos(sl, sc) && m_buffer->plainLine(sl)->matchesAt(sc, startComment) && previousNonSpaceCharPos(el, ec) && ((ec - endCommentLen + 1) >= 0) && m_buffer->plainLine(el)->matchesAt(ec - endCommentLen + 1, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(el, ec - endCommentLen + 1, el, ec + 1)); removeText(KTextEditor::Range(sl, sc, sl, sc + startCommentLen)); editEnd(); // selection automatically updated (MovingRange) } return remove; } bool KTextEditor::DocumentPrivate::removeStartStopCommentFromRegion(const KTextEditor::Cursor &start, const KTextEditor::Cursor &end, int attrib) { const QString startComment = highlight()->getCommentStart(attrib); const QString endComment = highlight()->getCommentEnd(attrib); const int startCommentLen = startComment.length(); const int endCommentLen = endComment.length(); const bool remove = m_buffer->plainLine(start.line())->matchesAt(start.column(), startComment) && m_buffer->plainLine(end.line())->matchesAt(end.column() - endCommentLen, endComment); if (remove) { editStart(); removeText(KTextEditor::Range(end.line(), end.column() - endCommentLen, end.line(), end.column())); removeText(KTextEditor::Range(start, startCommentLen)); editEnd(); } return remove; } /* Remove from the beginning of each line of the selection a start comment line mark. */ bool KTextEditor::DocumentPrivate::removeStartLineCommentFromSelection(KTextEditor::ViewPrivate *view, int attrib) { const QString shortCommentMark = highlight()->getCommentSingleLineStart(attrib); const QString longCommentMark = shortCommentMark + QLatin1Char(' '); int sl = view->selectionRange().start().line(); int el = view->selectionRange().end().line(); if ((view->selectionRange().end().column() == 0) && (el > 0)) { el--; } bool removed = false; editStart(); // For each line of the selection for (int z = el; z >= sl; z--) { // Try to remove the long comment mark first removed = (removeStringFromBeginning(z, longCommentMark) || removeStringFromBeginning(z, shortCommentMark) || removed); } editEnd(); // selection automatically updated (MovingRange) return removed; } /* Comment or uncomment the selection or the current line if there is no selection. */ void KTextEditor::DocumentPrivate::comment(KTextEditor::ViewPrivate *v, uint line, uint column, int change) { // skip word wrap bug #105373 const bool skipWordWrap = wordWrap(); if (skipWordWrap) { setWordWrap(false); } bool hassel = v->selection(); int c = 0; if (hassel) { c = v->selectionRange().start().column(); } int startAttrib = 0; Kate::TextLine ln = kateTextLine(line); if (c < ln->length()) { startAttrib = ln->attribute(c); } else if (!ln->attributesList().isEmpty()) { startAttrib = ln->attributesList().back().attributeValue; } bool hasStartLineCommentMark = !(highlight()->getCommentSingleLineStart(startAttrib).isEmpty()); bool hasStartStopCommentMark = (!(highlight()->getCommentStart(startAttrib).isEmpty()) && !(highlight()->getCommentEnd(startAttrib).isEmpty())); if (change > 0) { // comment if (!hassel) { if (hasStartLineCommentMark) { addStartLineCommentToSingleLine(line, startAttrib); } else if (hasStartStopCommentMark) { addStartStopCommentToSingleLine(line, startAttrib); } } else { // anders: prefer single line comment to avoid nesting probs // If the selection starts after first char in the first line // or ends before the last char of the last line, we may use // multiline comment markers. // TODO We should try to detect nesting. // - if selection ends at col 0, most likely she wanted that // line ignored const KTextEditor::Range sel = v->selectionRange(); if (hasStartStopCommentMark && (!hasStartLineCommentMark || ( (sel.start().column() > m_buffer->plainLine(sel.start().line())->firstChar()) || (sel.end().column() > 0 && sel.end().column() < (m_buffer->plainLine(sel.end().line())->length())) ))) { addStartStopCommentToSelection(v, startAttrib); } else if (hasStartLineCommentMark) { addStartLineCommentToSelection(v, startAttrib); } } } else { // uncomment bool removed = false; if (!hassel) { removed = (hasStartLineCommentMark && removeStartLineCommentFromSingleLine(line, startAttrib)) || (hasStartStopCommentMark && removeStartStopCommentFromSingleLine(line, startAttrib)); } else { // anders: this seems like it will work with above changes :) removed = (hasStartStopCommentMark && removeStartStopCommentFromSelection(v, startAttrib)) || (hasStartLineCommentMark && removeStartLineCommentFromSelection(v, startAttrib)); } // recursive call for toggle comment if (!removed && change == 0) { comment(v, line, column, 1); } } if (skipWordWrap) { setWordWrap(true); // see begin of function ::comment (bug #105373) } } void KTextEditor::DocumentPrivate::transform(KTextEditor::ViewPrivate *v, const KTextEditor::Cursor &c, KTextEditor::DocumentPrivate::TextTransform t) { if (v->selection()) { editStart(); // cache the selection and cursor, so we can be sure to restore. KTextEditor::Range selection = v->selectionRange(); KTextEditor::Range range(selection.start(), 0); while (range.start().line() <= selection.end().line()) { int start = 0; int end = lineLength(range.start().line()); if (range.start().line() == selection.start().line() || v->blockSelection()) { start = selection.start().column(); } if (range.start().line() == selection.end().line() || v->blockSelection()) { end = selection.end().column(); } if (start > end) { int swapCol = start; start = end; end = swapCol; } range.setStart(KTextEditor::Cursor(range.start().line(), start)); range.setEnd(KTextEditor::Cursor(range.end().line(), end)); QString s = text(range); QString old = s; if (t == Uppercase) { s = s.toUpper(); } else if (t == Lowercase) { s = s.toLower(); } else { // Capitalize Kate::TextLine l = m_buffer->plainLine(range.start().line()); int p(0); while (p < s.length()) { // If bol or the character before is not in a word, up this one: // 1. if both start and p is 0, upper char. // 2. if blockselect or first line, and p == 0 and start-1 is not in a word, upper // 3. if p-1 is not in a word, upper. if ((! range.start().column() && ! p) || ((range.start().line() == selection.start().line() || v->blockSelection()) && ! p && ! highlight()->isInWord(l->at(range.start().column() - 1))) || (p && ! highlight()->isInWord(s.at(p - 1))) ) { s[p] = s.at(p).toUpper(); } p++; } } if (s != old) { removeText(range); insertText(range.start(), s); } range.setBothLines(range.start().line() + 1); } editEnd(); // restore selection & cursor v->setSelection(selection); v->setCursorPosition(c); } else { // no selection editStart(); // get cursor KTextEditor::Cursor cursor = c; QString old = text(KTextEditor::Range(cursor, 1)); QString s; switch (t) { case Uppercase: s = old.toUpper(); break; case Lowercase: s = old.toLower(); break; case Capitalize: { Kate::TextLine l = m_buffer->plainLine(cursor.line()); while (cursor.column() > 0 && highlight()->isInWord(l->at(cursor.column() - 1), l->attribute(cursor.column() - 1))) { cursor.setColumn(cursor.column() - 1); } old = text(KTextEditor::Range(cursor, 1)); s = old.toUpper(); } break; default: break; } removeText(KTextEditor::Range(cursor, 1)); insertText(cursor, s); editEnd(); } } void KTextEditor::DocumentPrivate::joinLines(uint first, uint last) { // if ( first == last ) last += 1; editStart(); int line(first); while (first < last) { // Normalize the whitespace in the joined lines by making sure there's // always exactly one space between the joined lines // This cannot be done in editUnwrapLine, because we do NOT want this // behavior when deleting from the start of a line, just when explicitly // calling the join command Kate::TextLine l = kateTextLine(line); Kate::TextLine tl = kateTextLine(line + 1); if (!l || !tl) { editEnd(); return; } int pos = tl->firstChar(); if (pos >= 0) { if (pos != 0) { editRemoveText(line + 1, 0, pos); } if (!(l->length() == 0 || l->at(l->length() - 1).isSpace())) { - editInsertText(line + 1, 0, QLatin1String(" ")); + editInsertText(line + 1, 0, QStringLiteral(" ")); } } else { // Just remove the whitespace and let Kate handle the rest editRemoveText(line + 1, 0, tl->length()); } editUnWrapLine(line); first++; } editEnd(); } void KTextEditor::DocumentPrivate::tagLines(int start, int end) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->tagLines(start, end, true); } } void KTextEditor::DocumentPrivate::repaintViews(bool paintOnlyDirty) { foreach (KTextEditor::ViewPrivate *view, m_views) { view->repaintText(paintOnlyDirty); } } /* Bracket matching uses the following algorithm: If in overwrite mode, match the bracket currently underneath the cursor. Otherwise, if the character to the left is a bracket, match it. Otherwise if the character to the right of the cursor is a bracket, match it. Otherwise, don't match anything. */ KTextEditor::Range KTextEditor::DocumentPrivate::findMatchingBracket(const KTextEditor::Cursor &start, int maxLines) { if (maxLines < 0) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = m_buffer->plainLine(start.line()); if (!textLine) { return KTextEditor::Range::invalid(); } KTextEditor::Range range(start, start); const QChar right = textLine->at(range.start().column()); const QChar left = textLine->at(range.start().column() - 1); QChar bracket; if (config()->ovr()) { if (isBracket(right)) { bracket = right; } else { return KTextEditor::Range::invalid(); } } else if (isBracket(right)) { bracket = right; } else if (isBracket(left)) { range.setStart(KTextEditor::Cursor(range.start().line(), range.start().column() - 1)); bracket = left; } else { return KTextEditor::Range::invalid(); } const QChar opposite = matchingBracket(bracket); if (opposite.isNull()) { return KTextEditor::Range::invalid(); } const int searchDir = isStartBracket(bracket) ? 1 : -1; uint nesting = 0; const int minLine = qMax(range.start().line() - maxLines, 0); const int maxLine = qMin(range.start().line() + maxLines, documentEnd().line()); range.setEnd(range.start()); KTextEditor::DocumentCursor cursor(this); cursor.setPosition(range.start()); int validAttr = kateTextLine(cursor.line())->attribute(cursor.column()); while (cursor.line() >= minLine && cursor.line() <= maxLine) { if (!cursor.move(searchDir)) { return KTextEditor::Range::invalid(); } Kate::TextLine textLine = kateTextLine(cursor.line()); if (textLine->attribute(cursor.column()) == validAttr) { // Check for match QChar c = textLine->at(cursor.column()); if (c == opposite) { if (nesting == 0) { if (searchDir > 0) { // forward range.setEnd(cursor.toCursor()); } else { range.setStart(cursor.toCursor()); } return range; } nesting--; } else if (c == bracket) { nesting++; } } } return KTextEditor::Range::invalid(); } // helper: remove \r and \n from visible document name (bug #170876) inline static QString removeNewLines(const QString &str) { QString tmp(str); return tmp.replace(QLatin1String("\r\n"), QLatin1String(" ")) .replace(QLatin1Char('\r'), QLatin1Char(' ')) .replace(QLatin1Char('\n'), QLatin1Char(' ')); } void KTextEditor::DocumentPrivate::updateDocName() { // if the name is set, and starts with FILENAME, it should not be changed! if (! url().isEmpty() && (m_docName == removeNewLines(url().fileName()) || m_docName.startsWith(removeNewLines(url().fileName()) + QLatin1String(" (")))) { return; } int count = -1; foreach (KTextEditor::DocumentPrivate *doc, KTextEditor::EditorPrivate::self()->kateDocuments()) { if ((doc != this) && (doc->url().fileName() == url().fileName())) if (doc->m_docNameNumber > count) { count = doc->m_docNameNumber; } } m_docNameNumber = count + 1; QString oldName = m_docName; m_docName = removeNewLines(url().fileName()); m_isUntitled = m_docName.isEmpty(); if (m_isUntitled) { m_docName = i18n("Untitled"); } if (m_docNameNumber > 0) { m_docName = QString(m_docName + QLatin1String(" (%1)")).arg(m_docNameNumber + 1); } /** * avoid to emit this, if name doesn't change! */ if (oldName != m_docName) { emit documentNameChanged(this); } } void KTextEditor::DocumentPrivate::slotModifiedOnDisk(KTextEditor::View * /*v*/) { if (url().isEmpty() || !m_modOnHd) { return; } if (!isModified() && isAutoReload()) { onModOnHdAutoReload(); return; } if (!m_fileChangedDialogsActivated || m_modOnHdHandler) { return; } // don't ask the user again and again the same thing if (m_modOnHdReason == m_prevModOnHdReason) { return; } m_prevModOnHdReason = m_modOnHdReason; m_modOnHdHandler = new KateModOnHdPrompt(this, m_modOnHdReason, reasonedMOHString()); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::saveAsTriggered, this, &DocumentPrivate::onModOnHdSaveAs); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::closeTriggered, this, &DocumentPrivate::onModOnHdClose); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::reloadTriggered, this, &DocumentPrivate::onModOnHdReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::autoReloadTriggered, this, &DocumentPrivate::onModOnHdAutoReload); connect(m_modOnHdHandler.data(), &KateModOnHdPrompt::ignoreTriggered, this, &DocumentPrivate::onModOnHdIgnore); } void KTextEditor::DocumentPrivate::onModOnHdSaveAs() { m_modOnHd = false; QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File"), url()); if (!res.isEmpty()) { if (! saveAs(res)) { KMessageBox::error(parentWidget, i18n("Save failed")); m_modOnHd = true; } else { delete m_modOnHdHandler; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); } } else { // the save as dialog was canceled, we are still modified on disk m_modOnHd = true; } } void KTextEditor::DocumentPrivate::onModOnHdClose() { // avoid prompt in closeUrl() m_fileChangedDialogsActivated = false; // close the file without prompt confirmation closeUrl(); // Useful for kate only closeDocumentInApplication(); } void KTextEditor::DocumentPrivate::onModOnHdReload() { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::autoReloadToggled(bool b) { m_autoReloadMode->setChecked(b); if (b) { connect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } else { disconnect(&m_modOnHdTimer, &QTimer::timeout, this, &DocumentPrivate::onModOnHdAutoReload); } } bool KTextEditor::DocumentPrivate::isAutoReload() { return m_autoReloadMode->isChecked(); } void KTextEditor::DocumentPrivate::delayAutoReload() { if (isAutoReload()) { m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdAutoReload() { if (m_modOnHdHandler) { delete m_modOnHdHandler; autoReloadToggled(true); } if (!isAutoReload()) { return; } if (m_modOnHd && !m_reloading && !m_autoReloadThrottle.isActive()) { m_modOnHd = false; m_prevModOnHdReason = OnDiskUnmodified; emit modifiedOnDisk(this, false, OnDiskUnmodified); documentReload(); m_autoReloadThrottle.start(); } } void KTextEditor::DocumentPrivate::onModOnHdIgnore() { // ignore as long as m_prevModOnHdReason == m_modOnHdReason delete m_modOnHdHandler; } void KTextEditor::DocumentPrivate::setModifiedOnDisk(ModifiedOnDiskReason reason) { m_modOnHdReason = reason; m_modOnHd = (reason != OnDiskUnmodified); emit modifiedOnDisk(this, (reason != OnDiskUnmodified), reason); } class KateDocumentTmpMark { public: QString line; KTextEditor::Mark mark; }; void KTextEditor::DocumentPrivate::setModifiedOnDiskWarning(bool on) { m_fileChangedDialogsActivated = on; } bool KTextEditor::DocumentPrivate::documentReload() { if (url().isEmpty()) { return false; } // typically, the message for externally modified files is visible. Since it // does not make sense showing an additional dialog, just hide the message. delete m_modOnHdHandler; emit aboutToReload(this); QList tmp; for (QHash::const_iterator i = m_marks.constBegin(); i != m_marks.constEnd(); ++i) { KateDocumentTmpMark m; m.line = line(i.value()->line); m.mark = *i.value(); tmp.append(m); } const QString oldMode = mode(); const bool byUser = m_fileTypeSetByUser; const QString hl_mode = highlightingMode(); m_storedVariables.clear(); // save cursor positions for all views QHash cursorPositions; for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); cursorPositions.insert(v, v->cursorPosition()); } m_reloading = true; KTextEditor::DocumentPrivate::openUrl(url()); // reset some flags only valid for one reload! m_userSetEncodingForNextReload = false; // restore cursor positions for all views for (auto it = m_views.constBegin(); it != m_views.constEnd(); ++it) { auto v = it.value(); setActiveView(v); v->setCursorPosition(cursorPositions.value(v)); if (v->isVisible()) { v->repaintText(false); } } for (int z = 0; z < tmp.size(); z++) { if (z < lines()) { if (line(tmp.at(z).mark.line) == tmp.at(z).line) { setMark(tmp.at(z).mark.line, tmp.at(z).mark.type); } } } if (byUser) { setMode(oldMode); } setHighlightingMode(hl_mode); emit reloaded(this); return true; } bool KTextEditor::DocumentPrivate::documentSave() { if (!url().isValid() || !isReadWrite()) { return documentSaveAs(); } return save(); } bool KTextEditor::DocumentPrivate::documentSaveAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveAsWithEncoding(const QString &encoding) { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save File"), url()); if (saveUrl.isEmpty()) { return false; } setEncoding(encoding); return saveAs(saveUrl); } bool KTextEditor::DocumentPrivate::documentSaveCopyAs() { const QUrl saveUrl = QFileDialog::getSaveFileUrl(dialogParent(), i18n("Save Copy of File"), url()); if (saveUrl.isEmpty()) { return false; } QTemporaryFile file; if (!file.open()) { return false; } if (!m_buffer->saveFile(file.fileName())) { KMessageBox::error(dialogParent(), i18n("The document could not be saved, as it was not possible to write to %1.\n\nCheck that you have write access to this file or that enough disk space is available.", this->url().toDisplayString(QUrl::PreferLocalFile))); return false; } // get the right permissions, start with safe default KIO::StatJob *statJob = KIO::stat(url(), KIO::StatJob::SourceSide, 2); KJobWidgets::setWindow(statJob, QApplication::activeWindow()); int permissions = -1; if (statJob->exec()) { permissions = KFileItem(statJob->statResult(), url()).permissions(); } // KIO move, important: allow overwrite, we checked above! KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file.fileName()), saveUrl, permissions, KIO::Overwrite); KJobWidgets::setWindow(job, QApplication::activeWindow()); return job->exec(); } void KTextEditor::DocumentPrivate::setWordWrap(bool on) { config()->setWordWrap(on); } bool KTextEditor::DocumentPrivate::wordWrap() const { return config()->wordWrap(); } void KTextEditor::DocumentPrivate::setWordWrapAt(uint col) { config()->setWordWrapAt(col); } unsigned int KTextEditor::DocumentPrivate::wordWrapAt() const { return config()->wordWrapAt(); } void KTextEditor::DocumentPrivate::setPageUpDownMovesCursor(bool on) { config()->setPageUpDownMovesCursor(on); } bool KTextEditor::DocumentPrivate::pageUpDownMovesCursor() const { return config()->pageUpDownMovesCursor(); } //END bool KTextEditor::DocumentPrivate::setEncoding(const QString &e) { return m_config->setEncoding(e); } QString KTextEditor::DocumentPrivate::encoding() const { return m_config->encoding(); } void KTextEditor::DocumentPrivate::updateConfig() { m_undoManager->updateConfig(); // switch indenter if needed and update config.... m_indenter->setMode(m_config->indentationMode()); m_indenter->updateConfig(); // set tab width there, too m_buffer->setTabWidth(config()->tabWidth()); // update all views, does tagAll and updateView... foreach (KTextEditor::ViewPrivate *view, m_views) { view->updateDocumentConfig(); } // update on-the-fly spell checking as spell checking defaults might have changes if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); } emit configChanged(); } //BEGIN Variable reader // "local variable" feature by anders, 2003 /* TODO add config options (how many lines to read, on/off) add interface for plugins/apps to set/get variables add view stuff */ void KTextEditor::DocumentPrivate::readVariables(bool onlyViewAndRenderer) { if (!onlyViewAndRenderer) { m_config->configStart(); } // views! KTextEditor::ViewPrivate *v; foreach (v, m_views) { v->config()->configStart(); v->renderer()->config()->configStart(); } // read a number of lines in the top/bottom of the document for (int i = 0; i < qMin(9, lines()); ++i) { readVariableLine(line(i), onlyViewAndRenderer); } if (lines() > 10) { for (int i = qMax(10, lines() - 10); i < lines(); i++) { readVariableLine(line(i), onlyViewAndRenderer); } } if (!onlyViewAndRenderer) { m_config->configEnd(); } foreach (v, m_views) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } void KTextEditor::DocumentPrivate::readVariableLine(QString t, bool onlyViewAndRenderer) { static const QRegularExpression kvLine(QStringLiteral("kate:(.*)")); static const QRegularExpression kvLineWildcard(QStringLiteral("kate-wildcard\\((.*)\\):(.*)")); static const QRegularExpression kvLineMime(QStringLiteral("kate-mimetype\\((.*)\\):(.*)")); static const QRegularExpression kvVar(QStringLiteral("([\\w\\-]+)\\s+([^;]+)")); // simple check first, no regex // no kate inside, no vars, simple... if (!t.contains(QLatin1String("kate"))) { return; } // found vars, if any QString s; // now, try first the normal ones auto match = kvLine.match(t); if (match.hasMatch()) { s = match.captured(1); //qCDebug(LOG_KTE) << "normal variable line kate: matched: " << s; } else if ((match = kvLineWildcard.match(t)).hasMatch()) { // regex given const QStringList wildcards(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); const QString nameOfFile = url().fileName(); bool found = false; foreach (const QString &pattern, wildcards) { QRegExp wildcard(pattern, Qt::CaseSensitive, QRegExp::Wildcard); found = wildcard.exactMatch(nameOfFile); // Qt 5.12, no ifdef, more to test //QRegularExpression wildcard(QLatin1Char('^') + QRegularExpression::wildcardToRegularExpression(pattern) + QLatin1Char('$')); //found = wildcard.match(nameOfFile).hasMatch(); if (found) { break; } } // nothing usable found... if (!found) { return; } s = match.captured(2); //qCDebug(LOG_KTE) << "guarded variable line kate-wildcard: matched: " << s; } else if ((match = kvLineMime.match(t)).hasMatch()) { // mime-type given const QStringList types(match.captured(1).split(QLatin1Char(';'), QString::SkipEmptyParts)); // no matching type found if (!types.contains(mimeType())) { return; } s = match.captured(2); //qCDebug(LOG_KTE) << "guarded variable line kate-mimetype: matched: " << s; } else { // nothing found return; } // view variable names static const auto vvl = { QLatin1String("dynamic-word-wrap") , QLatin1String("dynamic-word-wrap-indicators") , QLatin1String("line-numbers") , QLatin1String("icon-border") , QLatin1String("folding-markers") , QLatin1String("folding-preview") , QLatin1String("bookmark-sorting") , QLatin1String("auto-center-lines") , QLatin1String("icon-bar-color") , QLatin1String("scrollbar-minimap") , QLatin1String("scrollbar-preview") // renderer , QLatin1String("background-color") , QLatin1String("selection-color") , QLatin1String("current-line-color") , QLatin1String("bracket-highlight-color") , QLatin1String("word-wrap-marker-color") , QLatin1String("font") , QLatin1String("font-size") , QLatin1String("scheme") }; int spaceIndent = -1; // for backward compatibility; see below bool replaceTabsSet = false; int startPos(0); QString var, val; while ((match = kvVar.match(s, startPos)).hasMatch()) { startPos = match.capturedEnd(0); var = match.captured(1); val = match.captured(2).trimmed(); bool state; // store booleans here int n; // store ints here // only apply view & renderer config stuff if (onlyViewAndRenderer) { if (contains(vvl, var)) { // FIXME define above setViewVariable(var, val); } } else { // BOOL SETTINGS if (var == QLatin1String("word-wrap") && checkBoolValue(val, &state)) { setWordWrap(state); // ??? FIXME CHECK } // KateConfig::configFlags // FIXME should this be optimized to only a few calls? how? else if (var == QLatin1String("backspace-indents") && checkBoolValue(val, &state)) { m_config->setBackspaceIndents(state); } else if (var == QLatin1String("indent-pasted-text") && checkBoolValue(val, &state)) { m_config->setIndentPastedText(state); } else if (var == QLatin1String("replace-tabs") && checkBoolValue(val, &state)) { m_config->setReplaceTabsDyn(state); replaceTabsSet = true; // for backward compatibility; see below } else if (var == QLatin1String("remove-trailing-space") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n("Using deprecated modeline 'remove-trailing-space'. " "Please replace with 'remove-trailing-spaces modified;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 1 : 0); } else if (var == QLatin1String("replace-trailing-space-save") && checkBoolValue(val, &state)) { qCWarning(LOG_KTE) << i18n("Using deprecated modeline 'replace-trailing-space-save'. " "Please replace with 'remove-trailing-spaces all;', see " "https://docs.kde.org/stable5/en/applications/katepart/config-variables.html#variable-remove-trailing-spaces"); m_config->setRemoveSpaces(state ? 2 : 0); } else if (var == QLatin1String("overwrite-mode") && checkBoolValue(val, &state)) { m_config->setOvr(state); } else if (var == QLatin1String("keep-extra-spaces") && checkBoolValue(val, &state)) { m_config->setKeepExtraSpaces(state); } else if (var == QLatin1String("tab-indents") && checkBoolValue(val, &state)) { m_config->setTabIndents(state); } else if (var == QLatin1String("show-tabs") && checkBoolValue(val, &state)) { m_config->setShowTabs(state); } else if (var == QLatin1String("show-trailing-spaces") && checkBoolValue(val, &state)) { m_config->setShowSpaces(state ? KateDocumentConfig::Trailing : KateDocumentConfig::None); } else if (var == QLatin1String("space-indent") && checkBoolValue(val, &state)) { // this is for backward compatibility; see below spaceIndent = state; } else if (var == QLatin1String("smart-home") && checkBoolValue(val, &state)) { m_config->setSmartHome(state); } else if (var == QLatin1String("newline-at-eof") && checkBoolValue(val, &state)) { m_config->setNewLineAtEof(state); } // INTEGER SETTINGS else if (var == QLatin1String("tab-width") && checkIntValue(val, &n)) { m_config->setTabWidth(n); } else if (var == QLatin1String("indent-width") && checkIntValue(val, &n)) { m_config->setIndentationWidth(n); } else if (var == QLatin1String("indent-mode")) { m_config->setIndentationMode(val); } else if (var == QLatin1String("word-wrap-column") && checkIntValue(val, &n) && n > 0) { // uint, but hard word wrap at 0 will be no fun ;) m_config->setWordWrapAt(n); } // STRING SETTINGS else if (var == QLatin1String("eol") || var == QLatin1String("end-of-line")) { const auto l = { QLatin1String("unix"), QLatin1String("dos"), QLatin1String("mac") }; if ((n = indexOf(l, val.toLower())) != -1) { /** * set eol + avoid that it is overwritten by auto-detection again! * this fixes e.g. .kateconfig files with // kate: eol dos; to work, bug 365705 */ m_config->setEol(n); m_config->setAllowEolDetection(false); } } else if (var == QLatin1String("bom") || var == QLatin1String("byte-order-mark") || var == QLatin1String("byte-order-marker")) { if (checkBoolValue(val, &state)) { m_config->setBom(state); } } else if (var == QLatin1String("remove-trailing-spaces")) { val = val.toLower(); if (val == QLatin1String("1") || val == QLatin1String("modified") || val == QLatin1String("mod") || val == QLatin1String("+")) { m_config->setRemoveSpaces(1); } else if (val == QLatin1String("2") || val == QLatin1String("all") || val == QLatin1String("*")) { m_config->setRemoveSpaces(2); } else { m_config->setRemoveSpaces(0); } } else if (var == QLatin1String("syntax") || var == QLatin1String("hl")) { setHighlightingMode(val); } else if (var == QLatin1String("mode")) { setMode(val); } else if (var == QLatin1String("encoding")) { setEncoding(val); } else if (var == QLatin1String("default-dictionary")) { setDefaultDictionary(val); } else if (var == QLatin1String("automatic-spell-checking") && checkBoolValue(val, &state)) { onTheFlySpellCheckingEnabled(state); } // VIEW SETTINGS else if (contains(vvl, var)) { setViewVariable(var, val); } else { m_storedVariables.insert(var, val); } } } // Backward compatibility // If space-indent was set, but replace-tabs was not set, we assume // that the user wants to replace tabulators and set that flag. // If both were set, replace-tabs has precedence. // At this point spaceIndent is -1 if it was never set, // 0 if it was set to off, and 1 if it was set to on. // Note that if onlyViewAndRenderer was requested, spaceIndent is -1. if (!replaceTabsSet && spaceIndent >= 0) { m_config->setReplaceTabsDyn(spaceIndent > 0); } } void KTextEditor::DocumentPrivate::setViewVariable(QString var, QString val) { KTextEditor::ViewPrivate *v; bool state; int n; QColor c; foreach (v, m_views) { // First, try the new config interface QVariant help(val); // Special treatment to catch "on"/"off" if (checkBoolValue(val, &state)) { help = state; } if (v->config()->setValue(var, help)) { } else if (v->renderer()->config()->setValue(var, help)) { // No success? Go the old way } else if (var == QLatin1String("dynamic-word-wrap") && checkBoolValue(val, &state)) { v->config()->setDynWordWrap(state); } else if (var == QLatin1String("block-selection") && checkBoolValue(val, &state)) { v->setBlockSelection(state); //else if ( var = "dynamic-word-wrap-indicators" ) } else if (var == QLatin1String("icon-bar-color") && checkColorValue(val, c)) { v->renderer()->config()->setIconBarColor(c); } // RENDERER else if (var == QLatin1String("background-color") && checkColorValue(val, c)) { v->renderer()->config()->setBackgroundColor(c); } else if (var == QLatin1String("selection-color") && checkColorValue(val, c)) { v->renderer()->config()->setSelectionColor(c); } else if (var == QLatin1String("current-line-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedLineColor(c); } else if (var == QLatin1String("bracket-highlight-color") && checkColorValue(val, c)) { v->renderer()->config()->setHighlightedBracketColor(c); } else if (var == QLatin1String("word-wrap-marker-color") && checkColorValue(val, c)) { v->renderer()->config()->setWordWrapMarkerColor(c); } else if (var == QLatin1String("font") || (checkIntValue(val, &n) && var == QLatin1String("font-size"))) { QFont _f(v->renderer()->config()->font()); if (var == QLatin1String("font")) { _f.setFamily(val); _f.setFixedPitch(QFont(val).fixedPitch()); } else { _f.setPointSize(n); } v->renderer()->config()->setFont(_f); } else if (var == QLatin1String("scheme")) { v->renderer()->config()->setSchema(val); } } } bool KTextEditor::DocumentPrivate::checkBoolValue(QString val, bool *result) { val = val.trimmed().toLower(); static const auto trueValues = { QLatin1String("1"), QLatin1String("on"), QLatin1String("true") }; if (contains(trueValues, val)) { *result = true; return true; } static const auto falseValues = { QLatin1String("0"), QLatin1String("off"), QLatin1String("false") }; if (contains(falseValues, val)) { *result = false; return true; } return false; } bool KTextEditor::DocumentPrivate::checkIntValue(QString val, int *result) { bool ret(false); *result = val.toInt(&ret); return ret; } bool KTextEditor::DocumentPrivate::checkColorValue(QString val, QColor &c) { c.setNamedColor(val); return c.isValid(); } // KTextEditor::variable QString KTextEditor::DocumentPrivate::variable(const QString &name) const { return m_storedVariables.value(name, QString()); } void KTextEditor::DocumentPrivate::setVariable(const QString &name, const QString &value) { QString s = QStringLiteral("kate: "); s.append(name); s.append(QLatin1Char(' ')); s.append(value); readVariableLine(s); } //END void KTextEditor::DocumentPrivate::slotModOnHdDirty(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskModified)) { m_modOnHd = true; m_modOnHdReason = OnDiskModified; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdCreated(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskCreated)) { m_modOnHd = true; m_modOnHdReason = OnDiskCreated; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotModOnHdDeleted(const QString &path) { if ((path == m_dirWatchFile) && (!m_modOnHd || m_modOnHdReason != OnDiskDeleted)) { m_modOnHd = true; m_modOnHdReason = OnDiskDeleted; if (!m_modOnHdTimer.isActive()) { m_modOnHdTimer.start(); } } } void KTextEditor::DocumentPrivate::slotDelayedHandleModOnHd() { // compare git hash with the one we have (if we have one) const QByteArray oldDigest = checksum(); if (!oldDigest.isEmpty() && !url().isEmpty() && url().isLocalFile()) { /** * if current checksum == checksum of new file => unmodified */ if (m_modOnHdReason != OnDiskDeleted && createDigest() && oldDigest == checksum()) { m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; } #if LIBGIT2_FOUND /** * if still modified, try to take a look at git * skip that, if document is modified! * only do that, if the file is still there, else reload makes no sense! */ if (m_modOnHd && !isModified() && QFile::exists(url().toLocalFile())) { /** * try to discover the git repo of this file * libgit2 docs state that UTF-8 is the right encoding, even on windows * I hope that is correct! */ git_repository *repository = nullptr; const QByteArray utf8Path = url().toLocalFile().toUtf8(); if (git_repository_open_ext(&repository, utf8Path.constData(), 0, nullptr) == 0) { /** * if we have repo, convert the git hash to an OID */ git_oid oid; if (git_oid_fromstr(&oid, oldDigest.toHex().data()) == 0) { /** * finally: is there a blob for this git hash? */ git_blob *blob = nullptr; if (git_blob_lookup(&blob, repository, &oid) == 0) { /** * this hash exists still in git => just reload */ m_modOnHd = false; m_modOnHdReason = OnDiskUnmodified; m_prevModOnHdReason = OnDiskUnmodified; documentReload(); } git_blob_free(blob); } } git_repository_free(repository); } #endif } /** * emit our signal to the outside! */ emit modifiedOnDisk(this, m_modOnHd, m_modOnHdReason); } QByteArray KTextEditor::DocumentPrivate::checksum() const { return m_buffer->digest(); } bool KTextEditor::DocumentPrivate::createDigest() { QByteArray digest; if (url().isLocalFile()) { QFile f(url().toLocalFile()); if (f.open(QIODevice::ReadOnly)) { // init the hash with the git header QCryptographicHash crypto(QCryptographicHash::Sha1); const QString header = QStringLiteral("blob %1").arg(f.size()); crypto.addData(header.toLatin1() + '\0'); while (!f.atEnd()) { crypto.addData(f.read(256 * 1024)); } digest = crypto.result(); } } /** * set new digest */ m_buffer->setDigest(digest); return !digest.isEmpty(); } QString KTextEditor::DocumentPrivate::reasonedMOHString() const { // squeeze path const QString str = KStringHandler::csqueeze(url().toDisplayString(QUrl::PreferLocalFile)); switch (m_modOnHdReason) { case OnDiskModified: return i18n("The file '%1' was modified by another program.", str); break; case OnDiskCreated: return i18n("The file '%1' was created by another program.", str); break; case OnDiskDeleted: return i18n("The file '%1' was deleted by another program.", str); break; default: return QString(); } Q_UNREACHABLE(); return QString(); } void KTextEditor::DocumentPrivate::removeTrailingSpaces() { const int remove = config()->removeSpaces(); if (remove == 0) { return; } // temporarily disable static word wrap (see bug #328900) const bool wordWrapEnabled = config()->wordWrap(); if (wordWrapEnabled) { setWordWrap(false); } editStart(); for (int line = 0; line < lines(); ++line) { Kate::TextLine textline = plainKateTextLine(line); // remove trailing spaces in entire document, remove = 2 // remove trailing spaces of touched lines, remove = 1 // remove trailing spaces of lines saved on disk, remove = 1 if (remove == 2 || textline->markedAsModified() || textline->markedAsSavedOnDisk()) { const int p = textline->lastChar() + 1; const int l = textline->length() - p; if (l > 0) { editRemoveText(line, p, l); } } } editEnd(); // enable word wrap again, if it was enabled (see bug #328900) if (wordWrapEnabled) { setWordWrap(true); // see begin of this function } } void KTextEditor::DocumentPrivate::updateFileType(const QString &newType, bool user) { if (user || !m_fileTypeSetByUser) { if (!newType.isEmpty()) { // remember that we got set by user m_fileTypeSetByUser = user; m_fileType = newType; m_config->configStart(); // NOTE: if the user changes the Mode, the Highlighting also changes. // m_hlSetByUser avoids resetting the highlight when saving the document, if // the current hl isn't stored (eg, in sftp:// or fish:// files) (see bug #407763) if ((user || !m_hlSetByUser) && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl.isEmpty()) { int hl(KateHlManager::self()->nameFind(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).hl)); if (hl >= 0) { m_buffer->setHighlight(hl); } } /** * set the indentation mode, if any in the mode... * and user did not set it before! * NOTE: KateBuffer::setHighlight() also sets the indentation. */ if (!m_indenterSetByUser && !KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter.isEmpty()) { config()->setIndentationMode(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).indenter); } // views! KTextEditor::ViewPrivate *v; foreach (v, m_views) { v->config()->configStart(); v->renderer()->config()->configStart(); } bool bom_settings = false; if (m_bomSetByUser) { bom_settings = m_config->bom(); } readVariableLine(KTextEditor::EditorPrivate::self()->modeManager()->fileType(newType).varLine); if (m_bomSetByUser) { m_config->setBom(bom_settings); } m_config->configEnd(); foreach (v, m_views) { v->config()->configEnd(); v->renderer()->config()->configEnd(); } } } // fixme, make this better... emit modeChanged(this); } void KTextEditor::DocumentPrivate::slotQueryClose_save(bool *handled, bool *abortClosing) { *handled = true; *abortClosing = true; if (this->url().isEmpty()) { QWidget *parentWidget(dialogParent()); const QUrl res = QFileDialog::getSaveFileUrl(parentWidget, i18n("Save File")); if (res.isEmpty()) { *abortClosing = true; return; } saveAs(res); *abortClosing = false; } else { save(); *abortClosing = false; } } //BEGIN KTextEditor::ConfigInterface // BEGIN ConfigInterface stff QStringList KTextEditor::DocumentPrivate::configKeys() const { /** * expose all internally registered keys of the KateDocumentConfig */ return m_config->configKeys(); } QVariant KTextEditor::DocumentPrivate::configValue(const QString &key) { /** * just dispatch to internal key => value lookup */ return m_config->value(key); } void KTextEditor::DocumentPrivate::setConfigValue(const QString &key, const QVariant &value) { /** * just dispatch to internal key + value set */ m_config->setValue(key, value); } //END KTextEditor::ConfigInterface KTextEditor::Cursor KTextEditor::DocumentPrivate::documentEnd() const { return KTextEditor::Cursor(lastLine(), lineLength(lastLine())); } bool KTextEditor::DocumentPrivate::replaceText(const KTextEditor::Range &range, const QString &s, bool block) { // TODO more efficient? editStart(); bool changed = removeText(range, block); changed |= insertText(range.start(), s, block); editEnd(); return changed; } KateHighlighting *KTextEditor::DocumentPrivate::highlight() const { return m_buffer->highlight(); } Kate::TextLine KTextEditor::DocumentPrivate::kateTextLine(int i) { m_buffer->ensureHighlighted(i); return m_buffer->plainLine(i); } Kate::TextLine KTextEditor::DocumentPrivate::plainKateTextLine(int i) { return m_buffer->plainLine(i); } bool KTextEditor::DocumentPrivate::isEditRunning() const { return editIsRunning; } void KTextEditor::DocumentPrivate::setUndoMergeAllEdits(bool merge) { if (merge && m_undoMergeAllEdits) { // Don't add another undo safe point: it will override our current one, // meaning we'll need two undo's to get back there - which defeats the object! return; } m_undoManager->undoSafePoint(); m_undoManager->setAllowComplexMerge(merge); m_undoMergeAllEdits = merge; } //BEGIN KTextEditor::MovingInterface KTextEditor::MovingCursor *KTextEditor::DocumentPrivate::newMovingCursor(const KTextEditor::Cursor &position, KTextEditor::MovingCursor::InsertBehavior insertBehavior) { return new Kate::TextCursor(buffer(), position, insertBehavior); } KTextEditor::MovingRange *KTextEditor::DocumentPrivate::newMovingRange(const KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior) { return new Kate::TextRange(buffer(), range, insertBehaviors, emptyBehavior); } qint64 KTextEditor::DocumentPrivate::revision() const { return m_buffer->history().revision(); } qint64 KTextEditor::DocumentPrivate::lastSavedRevision() const { return m_buffer->history().lastSavedRevision(); } void KTextEditor::DocumentPrivate::lockRevision(qint64 revision) { m_buffer->history().lockRevision(revision); } void KTextEditor::DocumentPrivate::unlockRevision(qint64 revision) { m_buffer->history().unlockRevision(revision); } void KTextEditor::DocumentPrivate::transformCursor(int &line, int &column, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); } void KTextEditor::DocumentPrivate::transformCursor(KTextEditor::Cursor &cursor, KTextEditor::MovingCursor::InsertBehavior insertBehavior, qint64 fromRevision, qint64 toRevision) { int line = cursor.line(), column = cursor.column(); m_buffer->history().transformCursor(line, column, insertBehavior, fromRevision, toRevision); cursor.setLine(line); cursor.setColumn(column); } void KTextEditor::DocumentPrivate::transformRange(KTextEditor::Range &range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors, KTextEditor::MovingRange::EmptyBehavior emptyBehavior, qint64 fromRevision, qint64 toRevision) { m_buffer->history().transformRange(range, insertBehaviors, emptyBehavior, fromRevision, toRevision); } //END //BEGIN KTextEditor::AnnotationInterface void KTextEditor::DocumentPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model) { KTextEditor::AnnotationModel *oldmodel = m_annotationModel; m_annotationModel = model; emit annotationModelChanged(oldmodel, m_annotationModel); } KTextEditor::AnnotationModel *KTextEditor::DocumentPrivate::annotationModel() const { return m_annotationModel; } //END KTextEditor::AnnotationInterface //TAKEN FROM kparts.h bool KTextEditor::DocumentPrivate::queryClose() { if (!isReadWrite() || !isModified()) { return true; } QString docName = documentName(); int res = KMessageBox::warningYesNoCancel(dialogParent(), i18n("The document \"%1\" has been modified.\n" "Do you want to save your changes or discard them?", docName), i18n("Close Document"), KStandardGuiItem::save(), KStandardGuiItem::discard()); bool abortClose = false; bool handled = false; switch (res) { case KMessageBox::Yes : sigQueryClose(&handled, &abortClose); if (!handled) { if (url().isEmpty()) { QUrl url = QFileDialog::getSaveFileUrl(dialogParent()); if (url.isEmpty()) { return false; } saveAs(url); } else { save(); } } else if (abortClose) { return false; } return waitSaveComplete(); case KMessageBox::No : return true; default : // case KMessageBox::Cancel : return false; } } void KTextEditor::DocumentPrivate::slotStarted(KIO::Job *job) { /** * if we are idle before, we are now loading! */ if (m_documentState == DocumentIdle) { m_documentState = DocumentLoading; } /** * if loading: * - remember pre loading read-write mode * if remote load: * - set to read-only * - trigger possible message */ if (m_documentState == DocumentLoading) { /** * remember state */ m_readWriteStateBeforeLoading = isReadWrite(); /** * perhaps show loading message, but wait one second */ if (job) { /** * only read only if really remote file! */ setReadWrite(false); /** * perhaps some message about loading in one second! * remember job pointer, we want to be able to kill it! */ m_loadingJob = job; QTimer::singleShot(1000, this, SLOT(slotTriggerLoadingMessage())); } } } void KTextEditor::DocumentPrivate::slotCompleted() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; } /** * Emit signal that we saved the document, if needed */ if (m_documentState == DocumentSaving || m_documentState == DocumentSavingAs) { emit documentSavedOrUploaded(this, m_documentState == DocumentSavingAs); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotCanceled() { /** * if were loading, reset back to old read-write mode before loading * and kill the possible loading message */ if (m_documentState == DocumentLoading) { setReadWrite(m_readWriteStateBeforeLoading); delete m_loadingMessage; showAndSetOpeningErrorAccess(); updateDocName(); } /** * back to idle mode */ m_documentState = DocumentIdle; m_reloading = false; } void KTextEditor::DocumentPrivate::slotTriggerLoadingMessage() { /** * no longer loading? * no message needed! */ if (m_documentState != DocumentLoading) { return; } /** * create message about file loading in progress */ delete m_loadingMessage; m_loadingMessage = new KTextEditor::Message(i18n("The file %2 is still loading.", url().toDisplayString(QUrl::PreferLocalFile), url().fileName())); m_loadingMessage->setPosition(KTextEditor::Message::TopInView); /** * if around job: add cancel action */ if (m_loadingJob) { QAction *cancel = new QAction(i18n("&Abort Loading"), nullptr); connect(cancel, SIGNAL(triggered()), this, SLOT(slotAbortLoading())); m_loadingMessage->addAction(cancel); } /** * really post message */ postMessage(m_loadingMessage); } void KTextEditor::DocumentPrivate::slotAbortLoading() { /** * no job, no work */ if (!m_loadingJob) { return; } /** * abort loading if any job * signal results! */ m_loadingJob->kill(KJob::EmitResult); m_loadingJob = nullptr; } void KTextEditor::DocumentPrivate::slotUrlChanged(const QUrl &url) { if (m_reloading) { // the URL is temporarily unset and then reset to the previous URL during reload // we do not want to notify the outside about this return; } Q_UNUSED(url); updateDocName(); emit documentUrlChanged(this); } bool KTextEditor::DocumentPrivate::save() { /** * no double save/load * we need to allow DocumentPreSavingAs here as state, as save is called in saveAs! */ if ((m_documentState != DocumentIdle) && (m_documentState != DocumentPreSavingAs)) { return false; } /** * if we are idle, we are now saving */ if (m_documentState == DocumentIdle) { m_documentState = DocumentSaving; } else { m_documentState = DocumentSavingAs; } /** * call back implementation for real work */ return KTextEditor::Document::save(); } bool KTextEditor::DocumentPrivate::saveAs(const QUrl &url) { /** * abort on bad URL * that is done in saveAs below, too * but we must check it here already to avoid messing up * as no signals will be send, then */ if (!url.isValid()) { return false; } /** * no double save/load */ if (m_documentState != DocumentIdle) { return false; } /** * we enter the pre save as phase */ m_documentState = DocumentPreSavingAs; /** * call base implementation for real work */ return KTextEditor::Document::saveAs(normalizeUrl(url)); } QString KTextEditor::DocumentPrivate::defaultDictionary() const { return m_defaultDictionary; } QList > KTextEditor::DocumentPrivate::dictionaryRanges() const { return m_dictionaryRanges; } void KTextEditor::DocumentPrivate::clearDictionaryRanges() { for (QList >::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end(); ++i) { delete(*i).first; } m_dictionaryRanges.clear(); if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(); } emit dictionaryRangesPresent(false); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range, bool blockmode) { if (blockmode) { for (int i = range.start().line(); i <= range.end().line(); ++i) { setDictionary(newDictionary, rangeOnLine(range, i)); } } else { setDictionary(newDictionary, range); } emit dictionaryRangesPresent(!m_dictionaryRanges.isEmpty()); } void KTextEditor::DocumentPrivate::setDictionary(const QString &newDictionary, const KTextEditor::Range &range) { KTextEditor::Range newDictionaryRange = range; if (!newDictionaryRange.isValid() || newDictionaryRange.isEmpty()) { return; } QList > newRanges; // all ranges is 'm_dictionaryRanges' are assumed to be mutually disjoint for (QList >::iterator i = m_dictionaryRanges.begin(); i != m_dictionaryRanges.end();) { qCDebug(LOG_KTE) << "new iteration" << newDictionaryRange; if (newDictionaryRange.isEmpty()) { break; } QPair pair = *i; QString dictionarySet = pair.second; KTextEditor::MovingRange *dictionaryRange = pair.first; qCDebug(LOG_KTE) << *dictionaryRange << dictionarySet; if (dictionaryRange->contains(newDictionaryRange) && newDictionary == dictionarySet) { qCDebug(LOG_KTE) << "dictionaryRange contains newDictionaryRange"; return; } if (newDictionaryRange.contains(*dictionaryRange)) { delete dictionaryRange; i = m_dictionaryRanges.erase(i); qCDebug(LOG_KTE) << "newDictionaryRange contains dictionaryRange"; continue; } KTextEditor::Range intersection = dictionaryRange->toRange().intersect(newDictionaryRange); if (!intersection.isEmpty() && intersection.isValid()) { if (dictionarySet == newDictionary) { // we don't have to do anything for 'intersection' // except cut off the intersection QList remainingRanges = KateSpellCheckManager::rangeDifference(newDictionaryRange, intersection); Q_ASSERT(remainingRanges.size() == 1); newDictionaryRange = remainingRanges.first(); ++i; qCDebug(LOG_KTE) << "dictionarySet == newDictionary"; continue; } QList remainingRanges = KateSpellCheckManager::rangeDifference(*dictionaryRange, intersection); for (QList::iterator j = remainingRanges.begin(); j != remainingRanges.end(); ++j) { KTextEditor::MovingRange *remainingRange = newMovingRange(*j, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); remainingRange->setFeedback(this); newRanges.push_back(QPair(remainingRange, dictionarySet)); } i = m_dictionaryRanges.erase(i); delete dictionaryRange; } else { ++i; } } m_dictionaryRanges += newRanges; if (!newDictionaryRange.isEmpty() && !newDictionary.isEmpty()) { // we don't add anything for the default dictionary KTextEditor::MovingRange *newDictionaryMovingRange = newMovingRange(newDictionaryRange, KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight); newDictionaryMovingRange->setFeedback(this); m_dictionaryRanges.push_back(QPair(newDictionaryMovingRange, newDictionary)); } if (m_onTheFlyChecker && !newDictionaryRange.isEmpty()) { m_onTheFlyChecker->refreshSpellCheck(newDictionaryRange); } } void KTextEditor::DocumentPrivate::setDefaultDictionary(const QString &dict) { if (m_defaultDictionary == dict) { return; } m_defaultDictionary = dict; if (m_onTheFlyChecker) { m_onTheFlyChecker->updateConfig(); refreshOnTheFlyCheck(); } emit defaultDictionaryChanged(this); } void KTextEditor::DocumentPrivate::onTheFlySpellCheckingEnabled(bool enable) { if (isOnTheFlySpellCheckingEnabled() == enable) { return; } if (enable) { Q_ASSERT(m_onTheFlyChecker == nullptr); m_onTheFlyChecker = new KateOnTheFlyChecker(this); } else { delete m_onTheFlyChecker; m_onTheFlyChecker = nullptr; } foreach (KTextEditor::ViewPrivate *view, m_views) { view->reflectOnTheFlySpellCheckStatus(enable); } } bool KTextEditor::DocumentPrivate::isOnTheFlySpellCheckingEnabled() const { return m_onTheFlyChecker != nullptr; } QString KTextEditor::DocumentPrivate::dictionaryForMisspelledRange(const KTextEditor::Range &range) const { if (!m_onTheFlyChecker) { return QString(); } else { return m_onTheFlyChecker->dictionaryForMisspelledRange(range); } } void KTextEditor::DocumentPrivate::clearMisspellingForWord(const QString &word) { if (m_onTheFlyChecker) { m_onTheFlyChecker->clearMisspellingForWord(word); } } void KTextEditor::DocumentPrivate::refreshOnTheFlyCheck(const KTextEditor::Range &range) { if (m_onTheFlyChecker) { m_onTheFlyChecker->refreshSpellCheck(range); } } void KTextEditor::DocumentPrivate::rangeInvalid(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::rangeEmpty(KTextEditor::MovingRange *movingRange) { deleteDictionaryRange(movingRange); } void KTextEditor::DocumentPrivate::deleteDictionaryRange(KTextEditor::MovingRange *movingRange) { qCDebug(LOG_KTE) << "deleting" << movingRange; auto finder = [=] (const QPair& item) -> bool { return item.first == movingRange; }; auto it = std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder); if (it != m_dictionaryRanges.end()) { m_dictionaryRanges.erase(it); delete movingRange; } Q_ASSERT(std::find_if(m_dictionaryRanges.begin(), m_dictionaryRanges.end(), finder) == m_dictionaryRanges.end()); } bool KTextEditor::DocumentPrivate::containsCharacterEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn; ++col) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); if (!prefixStore.findPrefix(textLine, col).isEmpty()) { return true; } } } return false; } int KTextEditor::DocumentPrivate::computePositionWrtOffsets(const OffsetList &offsetList, int pos) { int previousOffset = 0; for (OffsetList::const_iterator i = offsetList.begin(); i != offsetList.end(); ++i) { if ((*i).first > pos) { break; } previousOffset = (*i).second; } return pos + previousOffset; } QString KTextEditor::DocumentPrivate::decodeCharacters(const KTextEditor::Range &range, KTextEditor::DocumentPrivate::OffsetList &decToEncOffsetList, KTextEditor::DocumentPrivate::OffsetList &encToDecOffsetList) { QString toReturn; KTextEditor::Cursor previous = range.start(); int decToEncCurrentOffset = 0, encToDecCurrentOffset = 0; int i = 0; int newI = 0; KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const KatePrefixStore &prefixStore = highlighting->getCharacterEncodingsPrefixStore(attr); const QHash &characterEncodingsHash = highlighting->getCharacterEncodings(attr); QString matchingPrefix = prefixStore.findPrefix(textLine, col); if (!matchingPrefix.isEmpty()) { toReturn += text(KTextEditor::Range(previous, KTextEditor::Cursor(line, col))); const QChar &c = characterEncodingsHash.value(matchingPrefix); const bool isNullChar = c.isNull(); if (!c.isNull()) { toReturn += c; } i += matchingPrefix.length(); col += matchingPrefix.length(); previous = KTextEditor::Cursor(line, col); decToEncCurrentOffset = decToEncCurrentOffset - (isNullChar ? 0 : 1) + matchingPrefix.length(); encToDecCurrentOffset = encToDecCurrentOffset - matchingPrefix.length() + (isNullChar ? 0 : 1); newI += (isNullChar ? 0 : 1); decToEncOffsetList.push_back(QPair(newI, decToEncCurrentOffset)); encToDecOffsetList.push_back(QPair(i, encToDecCurrentOffset)); continue; } ++col; ++i; ++newI; } ++i; ++newI; } if (previous < range.end()) { toReturn += text(KTextEditor::Range(previous, range.end())); } return toReturn; } void KTextEditor::DocumentPrivate::replaceCharactersByEncoding(const KTextEditor::Range &range) { KateHighlighting *highlighting = highlight(); Kate::TextLine textLine; const int rangeStartLine = range.start().line(); const int rangeStartColumn = range.start().column(); const int rangeEndLine = range.end().line(); const int rangeEndColumn = range.end().column(); for (int line = range.start().line(); line <= rangeEndLine; ++line) { textLine = kateTextLine(line); int startColumn = (line == rangeStartLine) ? rangeStartColumn : 0; int endColumn = (line == rangeEndLine) ? rangeEndColumn : textLine->length(); for (int col = startColumn; col < endColumn;) { int attr = textLine->attribute(col); const QHash &reverseCharacterEncodingsHash = highlighting->getReverseCharacterEncodings(attr); QHash::const_iterator it = reverseCharacterEncodingsHash.find(textLine->at(col)); if (it != reverseCharacterEncodingsHash.end()) { replaceText(KTextEditor::Range(line, col, line, col + 1), *it); col += (*it).length(); continue; } ++col; } } } // // Highlighting information // KTextEditor::Attribute::Ptr KTextEditor::DocumentPrivate::attributeAt(const KTextEditor::Cursor &position) { KTextEditor::Attribute::Ptr attrib(new KTextEditor::Attribute()); KTextEditor::ViewPrivate *view = m_views.empty() ? nullptr : m_views.begin().value(); if (!view) { qCWarning(LOG_KTE) << "ATTENTION: cannot access lineAttributes() without any View (will be fixed eventually)"; return attrib; } Kate::TextLine kateLine = kateTextLine(position.line()); if (!kateLine) { return attrib; } *attrib = *view->renderer()->attribute(kateLine->attribute(position.column())); return attrib; } QStringList KTextEditor::DocumentPrivate::embeddedHighlightingModes() const { return highlight()->getEmbeddedHighlightingModes(); } QString KTextEditor::DocumentPrivate::highlightingModeAt(const KTextEditor::Cursor &position) { return highlight()->higlightingModeForLocation(this, position); } Kate::SwapFile *KTextEditor::DocumentPrivate::swapFile() { return m_swapfile; } /** * \return \c -1 if \c line or \c column invalid, otherwise one of * standard style attribute number */ int KTextEditor::DocumentPrivate::defStyleNum(int line, int column) { // Validate parameters to prevent out of range access if (line < 0 || line >= lines() || column < 0) { return -1; } // get highlighted line Kate::TextLine tl = kateTextLine(line); // make sure the textline is a valid pointer if (!tl) { return -1; } /** * either get char attribute or attribute of context still active at end of line */ int attribute = 0; if (column < tl->length()) { attribute = tl->attribute(column); } else if (column == tl->length()) { if (!tl->attributesList().isEmpty()) { attribute = tl->attributesList().back().attributeValue; } else { return -1; } } else { return -1; } return highlight()->defaultStyleForAttribute(attribute); } bool KTextEditor::DocumentPrivate::isComment(int line, int column) { const int defaultStyle = defStyleNum(line, column); return defaultStyle == KTextEditor::dsComment; } int KTextEditor::DocumentPrivate::findTouchedLine(int startLine, bool down) { const int offset = down ? 1 : -1; const int lineCount = lines(); while (startLine >= 0 && startLine < lineCount) { Kate::TextLine tl = m_buffer->plainLine(startLine); if (tl && (tl->markedAsModified() || tl->markedAsSavedOnDisk())) { return startLine; } startLine += offset; } return -1; } void KTextEditor::DocumentPrivate::setActiveTemplateHandler(KateTemplateHandler* handler) { // delete any active template handler delete m_activeTemplateHandler.data(); m_activeTemplateHandler = handler; } //BEGIN KTextEditor::MessageInterface bool KTextEditor::DocumentPrivate::postMessage(KTextEditor::Message *message) { // no message -> cancel if (!message) { return false; } // make sure the desired view belongs to this document if (message->view() && message->view()->document() != this) { qCWarning(LOG_KTE) << "trying to post a message to a view of another document:" << message->text(); return false; } message->setParent(this); message->setDocument(this); // if there are no actions, add a close action by default if widget does not auto-hide if (message->actions().count() == 0 && message->autoHide() < 0) { QAction *closeAction = new QAction(QIcon::fromTheme(QStringLiteral("window-close")), i18n("&Close"), nullptr); closeAction->setToolTip(i18n("Close message")); message->addAction(closeAction); } // make sure the message is registered even if no actions and no views exist m_messageHash[message] = QList >(); // reparent actions, as we want full control over when they are deleted foreach (QAction *action, message->actions()) { action->setParent(nullptr); m_messageHash[message].append(QSharedPointer(action)); } // post message to requested view, or to all views if (KTextEditor::ViewPrivate *view = qobject_cast(message->view())) { view->postMessage(message, m_messageHash[message]); } else { foreach (KTextEditor::ViewPrivate *view, m_views) { view->postMessage(message, m_messageHash[message]); } } // also catch if the user manually calls delete message connect(message, SIGNAL(closed(KTextEditor::Message*)), SLOT(messageDestroyed(KTextEditor::Message*))); return true; } void KTextEditor::DocumentPrivate::messageDestroyed(KTextEditor::Message *message) { // KTE:Message is already in destructor Q_ASSERT(m_messageHash.contains(message)); m_messageHash.remove(message); } //END KTextEditor::MessageInterface void KTextEditor::DocumentPrivate::closeDocumentInApplication() { KTextEditor::EditorPrivate::self()->application()->closeDocument(this); } diff --git a/src/script/katescriptmanager.cpp b/src/script/katescriptmanager.cpp index 9bb3031f..be9bb76b 100644 --- a/src/script/katescriptmanager.cpp +++ b/src/script/katescriptmanager.cpp @@ -1,336 +1,336 @@ // This file is part of the KDE libraries // Copyright (C) 2005 Christoph Cullmann // Copyright (C) 2005 Joseph Wenninger // Copyright (C) 2006-2018 Dominik Haumann // Copyright (C) 2008 Paul Giannaros // Copyright (C) 2010 Joseph Wenninger // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License version 2 as published by the Free Software Foundation. // // This library 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 // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public License // along with this library; see the file COPYING.LIB. If not, write to // the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, // Boston, MA 02110-1301, USA. #include "katescriptmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kateglobal.h" #include "katecmd.h" #include "katepartdebug.h" KateScriptManager *KateScriptManager::m_instance = nullptr; KateScriptManager::KateScriptManager() : KTextEditor::Command({ QStringLiteral("reload-scripts") }) { // use cached info collect(); } KateScriptManager::~KateScriptManager() { qDeleteAll(m_indentationScripts); qDeleteAll(m_commandLineScripts); m_instance = nullptr; } KateIndentScript *KateScriptManager::indenter(const QString &language) { KateIndentScript *highestPriorityIndenter = nullptr; foreach (KateIndentScript *indenter, m_languageToIndenters.value(language.toLower())) { // don't overwrite if there is already a result with a higher priority if (highestPriorityIndenter && indenter->indentHeader().priority() < highestPriorityIndenter->indentHeader().priority()) { #ifdef DEBUG_SCRIPTMANAGER qCDebug(LOG_KTE) << "Not overwriting indenter for" << language << "as the priority isn't big enough (" << indenter->indentHeader().priority() << '<' << highestPriorityIndenter->indentHeader().priority() << ')'; #endif } else { highestPriorityIndenter = indenter; } } #ifdef DEBUG_SCRIPTMANAGER if (highestPriorityIndenter) { qCDebug(LOG_KTE) << "Found indenter" << highestPriorityIndenter->url() << "for" << language; } else { qCDebug(LOG_KTE) << "No indenter for" << language; } #endif return highestPriorityIndenter; } /** * Small helper: QJsonValue to QStringList */ static QStringList jsonToStringList (const QJsonValue &value) { QStringList list; Q_FOREACH (const QJsonValue &value, value.toArray()) { if (value.isString()) { list.append(value.toString()); } } return list; } void KateScriptManager::collect() { // clear out the old scripts and reserve enough space qDeleteAll(m_indentationScripts); qDeleteAll(m_commandLineScripts); m_indentationScripts.clear(); m_commandLineScripts.clear(); m_languageToIndenters.clear(); m_indentationScriptMap.clear(); /** * now, we search all kinds of known scripts */ for (const auto &type : { QLatin1String("indentation"), QLatin1String("commands") }) { // basedir for filesystem lookup const QString basedir = QLatin1String("/katepart5/script/") + type; QStringList dirs; // first writable locations, e.g. stuff the user has provided dirs += QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + basedir; // then resources, e.g. the stuff we ship with us dirs.append(QLatin1String(":/ktexteditor/script/") + type); // then all other locations, this includes global stuff installed by other applications // this will not allow global stuff to overwrite the stuff we ship in our resources to allow to install a more up-to-date ktexteditor lib locally! foreach (const QString &dir, QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) { dirs.append(dir + basedir); } QStringList list; foreach (const QString &dir, dirs) { const QStringList fileNames = QDir(dir).entryList({ QStringLiteral("*.js") }); foreach (const QString &file, fileNames) { list.append(dir + QLatin1Char('/') + file); } } // iterate through the files and read info out of cache or file, no double loading of same scripts QSet unique; foreach (const QString &fileName, list) { /** * get file basename */ const QString baseName = QFileInfo(fileName).baseName(); /** * only load scripts once, even if multiple installed variants found! */ if (unique.contains(baseName)) continue; /** * remember the script */ unique.insert (baseName); /** * open file or skip it */ QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qCDebug(LOG_KTE) << "Script parse error: Cannot open file " << qPrintable(fileName) << '\n'; continue; } /** * search json header or skip this file */ QByteArray fileContent = file.readAll(); int startOfJson = fileContent.indexOf ('{'); if (startOfJson < 0) { qCDebug(LOG_KTE) << "Script parse error: Cannot find start of json header at start of file " << qPrintable(fileName) << '\n'; continue; } int endOfJson = fileContent.indexOf("\n};", startOfJson); if (endOfJson < 0) { // as fallback, check also mac os line ending endOfJson = fileContent.indexOf("\r};", startOfJson); } if (endOfJson < 0) { qCDebug(LOG_KTE) << "Script parse error: Cannot find end of json header at start of file " << qPrintable(fileName) << '\n'; continue; } endOfJson += 2; //we want the end including the } but not the ; /** * parse json header or skip this file */ QJsonParseError error; const QJsonDocument metaInfo (QJsonDocument::fromJson(fileContent.mid(startOfJson, endOfJson-startOfJson), &error)); if (error.error || !metaInfo.isObject()) { qCDebug(LOG_KTE) << "Script parse error: Cannot parse json header at start of file " << qPrintable(fileName) << error.errorString() << endOfJson << fileContent.mid(endOfJson-25, 25).replace('\n', ' '); continue; } /** * remember type */ KateScriptHeader generalHeader; if (type == QLatin1String("indentation")) { generalHeader.setScriptType(Kate::ScriptType::Indentation); } else if (type == QLatin1String("commands")) { generalHeader.setScriptType(Kate::ScriptType::CommandLine); } else { // should never happen, we dictate type by directory Q_ASSERT(false); } const QJsonObject metaInfoObject = metaInfo.object(); generalHeader.setLicense(metaInfoObject.value(QStringLiteral("license")).toString()); generalHeader.setAuthor(metaInfoObject.value(QStringLiteral("author")).toString()); generalHeader.setRevision(metaInfoObject.value(QStringLiteral("revision")).toInt()); generalHeader.setKateVersion(metaInfoObject.value(QStringLiteral("kate-version")).toString()); // now, cast accordingly based on type switch (generalHeader.scriptType()) { case Kate::ScriptType::Indentation: { KateIndentScriptHeader indentHeader; indentHeader.setName(metaInfoObject.value(QStringLiteral("name")).toString()); indentHeader.setBaseName(baseName); if (indentHeader.name().isNull()) { qCDebug(LOG_KTE) << "Script value error: No name specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping indenter" << '\n'; continue; } // required style? indentHeader.setRequiredStyle(metaInfoObject.value(QStringLiteral("required-syntax-style")).toString()); // which languages does this support? QStringList indentLanguages = jsonToStringList(metaInfoObject.value(QStringLiteral("indent-languages"))); if (!indentLanguages.isEmpty()) { indentHeader.setIndentLanguages(indentLanguages); } else { indentHeader.setIndentLanguages(QStringList() << indentHeader.name()); #ifdef DEBUG_SCRIPTMANAGER qCDebug(LOG_KTE) << "Script value warning: No indent-languages specified for indent " << "script " << qPrintable(fileName) << ". Using the name (" << qPrintable(indentHeader.name()) << ")\n"; #endif } // priority indentHeader.setPriority(metaInfoObject.value(QStringLiteral("priority")).toInt()); KateIndentScript *script = new KateIndentScript(fileName, indentHeader); script->setGeneralHeader(generalHeader); foreach (const QString &language, indentHeader.indentLanguages()) { m_languageToIndenters[language.toLower()].push_back(script); } m_indentationScriptMap.insert(indentHeader.baseName(), script); m_indentationScripts.append(script); break; } case Kate::ScriptType::CommandLine: { KateCommandLineScriptHeader commandHeader; commandHeader.setFunctions(jsonToStringList(metaInfoObject.value(QStringLiteral("functions")))); commandHeader.setActions(metaInfoObject.value(QStringLiteral("actions")).toArray()); if (commandHeader.functions().isEmpty()) { qCDebug(LOG_KTE) << "Script value error: No functions specified in script meta data: " << qPrintable(fileName) << '\n' << "-> skipping script" << '\n'; continue; } KateCommandLineScript *script = new KateCommandLineScript(fileName, commandHeader); script->setGeneralHeader(generalHeader); m_commandLineScripts.push_back(script); break; } case Kate::ScriptType::Unknown: default: qCDebug(LOG_KTE) << "Script value warning: Unknown type ('" << qPrintable(type) << "'): " << qPrintable(fileName) << '\n'; } } } #ifdef DEBUG_SCRIPTMANAGER // XX Test if (indenter("Python")) { qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("triggerCharacters").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("triggerCharacters").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->global("blafldsjfklas").isValid() << "\n"; qCDebug(LOG_KTE) << "Python: " << indenter("Python")->function("indent").isValid() << "\n"; } if (indenter("C")) { qCDebug(LOG_KTE) << "C: " << qPrintable(indenter("C")->url()) << "\n"; } if (indenter("lisp")) { qCDebug(LOG_KTE) << "LISP: " << qPrintable(indenter("Lisp")->url()) << "\n"; } #endif } void KateScriptManager::reload() { collect(); emit reloaded(); } /// Kate::Command stuff bool KateScriptManager::exec(KTextEditor::View *view, const QString &_cmd, QString &errorMsg, const KTextEditor::Range &) { Q_UNUSED(view) - QVector args = _cmd.splitRef(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts); + QVector args = _cmd.splitRef(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts); const QString cmd = args.first().toString(); if (cmd == QLatin1String("reload-scripts")) { reload(); return true; } return false; } bool KateScriptManager::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view) if (cmd == QLatin1String("reload-scripts")) { msg = i18n("Reload all JavaScript files (indenters, command line scripts, etc)."); return true; } return false; } diff --git a/src/syntax/katehighlight.cpp b/src/syntax/katehighlight.cpp index 5aa52ee3..ae81f0a1 100644 --- a/src/syntax/katehighlight.cpp +++ b/src/syntax/katehighlight.cpp @@ -1,725 +1,725 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Mirko Stocker Copyright (C) 2007 Matthew Woehlke Copyright (C) 2003, 2004 Anders Lund Copyright (C) 2003 Hamish Rodda Copyright (C) 2001,2002 Joseph Wenninger Copyright (C) 2001 Christoph Cullmann Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //BEGIN INCLUDES #include "katehighlight.h" #include "katetextline.h" #include "katedocument.h" #include "katerenderer.h" #include "kateglobal.h" #include "kateschema.h" #include "kateconfig.h" #include "kateextendedattribute.h" #include "katepartdebug.h" #include "katedefaultcolors.h" #include #include #include #include #include #include #include #include #include //END //BEGIN STATICS namespace { /** * convert from KSyntaxHighlighting => KTextEditor type * special handle non-1:1 things */ inline KTextEditor::DefaultStyle textStyleToDefaultStyle(const KSyntaxHighlighting::Theme::TextStyle textStyle) { // handle deviations if (textStyle == KSyntaxHighlighting::Theme::Error) { return KTextEditor::dsError; } if (textStyle == KSyntaxHighlighting::Theme::Others) { return KTextEditor::dsOthers; } // else: simple cast return static_cast(textStyle); } /** * convert the theme/schema name from KTextEditor => KSyntaxHighlighting. * NOTE: some themes of KTextEditor don't exist in KSyntaxHighlighting */ inline QString convertThemeName(const QString &schema) { if (schema == QLatin1String("Normal")) { return QStringLiteral("Default"); } else if (schema == QLatin1String("Solarized (light)")) { return QStringLiteral("Solarized Light"); } else if (schema == QLatin1String("Solarized (dark)")) { return QStringLiteral("Solarized Dark"); } return schema; } } //END //BEGIN KateHighlighting KateHighlighting::KateHighlighting(const KSyntaxHighlighting::Definition &def) { /** * get name and section, always works */ iName = def.name(); iSection = def.translatedSection(); /** * get all included definitions, e.g. PHP for HTML highlighting */ auto definitions = def.includedDefinitions(); /** * handle the "no highlighting" case * it's possible to not have any defintions with malformed file */ if (!def.isValid() || (definitions.isEmpty() && def.formats().isEmpty())) { // dummy properties + formats m_properties.resize(1); m_propertiesForFormat.push_back(&m_properties[0]); m_formats.resize(1); m_formatsIdToIndex.insert(std::make_pair(m_formats[0].id(), 0)); // be done, all below is just for the real highlighting variants return; } /** * handle the real highlighting case */ noHl = false; iHidden = def.isHidden(); identifier = def.filePath(); iStyle = def.style(); m_indentation = def.indenter(); folding = def.foldingEnabled(); m_foldingIndentationSensitive = def.indentationBasedFoldingEnabled(); /** * tell the AbstractHighlighter the definition it shall use */ setDefinition(def); /** * first: handle only really included definitions */ for (const auto &includedDefinition : definitions) embeddedHighlightingModes.push_back(includedDefinition.name()); /** * now: handle all, including this definition itself * create the format => attributes mapping * collect embedded highlightings, too * * we start with our definition as we want to have the default format * of the initial definition as attribute with index == 0 * * we collect additional properties in the m_properties and * map the formats to the right properties in m_propertiesForFormat */ definitions.push_front(definition()); m_properties.resize(definitions.size()); size_t propertiesIndex = 0; for (const auto & includedDefinition : definitions) { auto &properties = m_properties[propertiesIndex]; properties.definition = includedDefinition; for (const auto &emptyLine : includedDefinition.foldingIgnoreList()) properties.emptyLines.push_back(QRegularExpression(emptyLine)); properties.singleLineCommentMarker = includedDefinition.singleLineCommentMarker(); properties.singleLineCommentPosition = includedDefinition.singleLineCommentPosition(); const auto multiLineComment = includedDefinition.multiLineCommentMarker(); properties.multiLineCommentStart = multiLineComment.first; properties.multiLineCommentEnd = multiLineComment.second; // collect character characters for (const auto &enc : includedDefinition.characterEncodings()) { properties.characterEncodingsPrefixStore.addPrefix(enc.second); properties.characterEncodings[enc.second] = enc.first; properties.reverseCharacterEncodings[enc.first] = enc.second; } // collect formats for (const auto & format : includedDefinition.formats()) { // register format id => internal attributes, we want no clashs const auto nextId = m_formats.size(); m_formatsIdToIndex.insert(std::make_pair(format.id(), nextId)); m_formats.push_back(format); m_propertiesForFormat.push_back(&properties); } // advance to next properties ++propertiesIndex; } } void KateHighlighting::doHighlight(const Kate::TextLineData *prevLine, Kate::TextLineData *textLine, const Kate::TextLineData *nextLine, bool &ctxChanged, int tabWidth) { // default: no context change ctxChanged = false; // no text line => nothing to do if (!textLine) { return; } // in all cases, remove old hl, or we will grow to infinite ;) textLine->clearAttributesAndFoldings(); // reset folding start textLine->clearMarkedAsFoldingStart(); // no hl set, nothing to do more than the above cleaning ;) if (noHl) { return; } /** * ensure we arrive in clean state */ Q_ASSERT(!m_textLineToHighlight); Q_ASSERT(m_foldingStartToCount.isEmpty()); /** * highlight the given line via the abstract highlighter * a bit ugly: we set the line to highlight as member to be able to update its stats in the applyFormat and applyFolding member functions */ m_textLineToHighlight = textLine; const KSyntaxHighlighting::State initialState (!prevLine ? KSyntaxHighlighting::State() : prevLine->highlightingState()); const KSyntaxHighlighting::State endOfLineState = highlightLine(textLine->string(), initialState); m_textLineToHighlight = nullptr; /** * update highlighting state if needed */ if (textLine->highlightingState() != endOfLineState) { textLine->setHighlightingState(endOfLineState); ctxChanged = true; } /** * handle folding info computed and cleanup hash again, if there * check if folding is not balanced and we have more starts then ends * then this line is a possible folding start! */ if (!m_foldingStartToCount.isEmpty()) { /** * possible folding start, if imbalanced, aka hash not empty! */ textLine->markAsFoldingStartAttribute(); /** * clear hash for next doHighlight */ m_foldingStartToCount.clear(); } /** * check for indentation based folding */ if (m_foldingIndentationSensitive && (tabWidth > 0) && !textLine->markedAsFoldingStartAttribute()) { /** * compute if we increase indentation in next line */ if (endOfLineState.indentationBasedFoldingEnabled() && !isEmptyLine(textLine) && !isEmptyLine(nextLine) && (textLine->indentDepth(tabWidth) < nextLine->indentDepth(tabWidth))) { textLine->markAsFoldingStartIndentation(); } } } void KateHighlighting::applyFormat(int offset, int length, const KSyntaxHighlighting::Format &format) { // WE ATM assume ascending offset order Q_ASSERT(m_textLineToHighlight); if (!format.isValid()) { return; } // get internal attribute, must exist const auto it = m_formatsIdToIndex.find(format.id()); Q_ASSERT(it != m_formatsIdToIndex.end()); // remember highlighting info in our textline m_textLineToHighlight->addAttribute(Kate::TextLineData::Attribute(offset, length, it->second)); } void KateHighlighting::applyFolding(int offset, int length, KSyntaxHighlighting::FoldingRegion region) { // WE ATM assume ascending offset order, we add the length to the offset for the folding ends to have ranges spanning the full folding region Q_ASSERT(m_textLineToHighlight); Q_ASSERT(region.isValid()); const int foldingValue = (region.type() == KSyntaxHighlighting::FoldingRegion::Begin) ? int(region.id()) : -int(region.id()); m_textLineToHighlight->addFolding(offset + ((region.type() == KSyntaxHighlighting::FoldingRegion::Begin) ? 0 : length), foldingValue); /** * for each end region, decrement counter for that type, erase if count reaches 0! */ if (foldingValue < 0) { QHash::iterator end = m_foldingStartToCount.find(-foldingValue); if (end != m_foldingStartToCount.end()) { if (end.value() > 1) { --(end.value()); } else { m_foldingStartToCount.erase(end); } } } /** * increment counter for each begin region! */ if (foldingValue > 0) { ++m_foldingStartToCount[foldingValue]; } } void KateHighlighting::getKateExtendedAttributeList(const QString &schema, QVector &list, KConfig *cfg) { KConfigGroup config(cfg ? cfg : KateHlManager::self()->getKConfig(), QLatin1String("Highlighting ") + iName + QLatin1String(" - Schema ") + schema); list = attributesForDefinition(schema); foreach (KTextEditor::Attribute::Ptr p, list) { Q_ASSERT(p); QStringList s = config.readEntry(p->name(), QStringList()); // qCDebug(LOG_KTE)<name<name(); bool spellCheck = !p->skipSpellChecking(); p->clear(); p->setName(name); p->setSkipSpellChecking(!spellCheck); QString tmp = s[0]; if (!tmp.isEmpty()) { p->setDefaultStyle(static_cast (tmp.toInt())); } QRgb col; tmp = s[1]; if (!tmp.isEmpty()) { col = tmp.toUInt(nullptr, 16); p->setForeground(QColor(col)); } tmp = s[2]; if (!tmp.isEmpty()) { col = tmp.toUInt(nullptr, 16); p->setSelectedForeground(QColor(col)); } tmp = s[3]; if (!tmp.isEmpty()) { p->setFontBold(tmp != QLatin1String("0")); } tmp = s[4]; if (!tmp.isEmpty()) { p->setFontItalic(tmp != QLatin1String("0")); } tmp = s[5]; if (!tmp.isEmpty()) { p->setFontStrikeOut(tmp != QLatin1String("0")); } tmp = s[6]; if (!tmp.isEmpty()) { p->setFontUnderline(tmp != QLatin1String("0")); } tmp = s[7]; if (!tmp.isEmpty()) { col = tmp.toUInt(nullptr, 16); p->setBackground(QColor(col)); } tmp = s[8]; if (!tmp.isEmpty()) { col = tmp.toUInt(nullptr, 16); p->setSelectedBackground(QColor(col)); } tmp = s[9]; if (!tmp.isEmpty() && tmp != QLatin1String("---")) { p->setFontFamily(tmp); } } } } void KateHighlighting::getKateExtendedAttributeListCopy(const QString &schema, QVector &list, KConfig *cfg) { QVector attributes; getKateExtendedAttributeList(schema, attributes, cfg); list.clear(); foreach (const KTextEditor::Attribute::Ptr &attribute, attributes) { list.append(KTextEditor::Attribute::Ptr(new KTextEditor::Attribute(*attribute.data()))); } } void KateHighlighting::setKateExtendedAttributeList(const QString &schema, QVector &list, KConfig *cfg, bool writeDefaultsToo) { KConfigGroup config(cfg ? cfg : KateHlManager::self()->getKConfig(), QLatin1String("Highlighting ") + iName + QLatin1String(" - Schema ") + schema); QStringList settings; KateAttributeList defList; KateHlManager::self()->getDefaults(schema, defList); foreach (const KTextEditor::Attribute::Ptr &p, list) { Q_ASSERT(p); settings.clear(); KTextEditor::DefaultStyle defStyle = p->defaultStyle(); KTextEditor::Attribute::Ptr a(defList[defStyle]); settings << QString::number(p->defaultStyle(), 10); settings << (p->hasProperty(QTextFormat::ForegroundBrush) ? QString::number(p->foreground().color().rgb(), 16) : (writeDefaultsToo ? QString::number(a->foreground().color().rgb(), 16) : QString())); settings << (p->hasProperty(SelectedForeground) ? QString::number(p->selectedForeground().color().rgb(), 16) : (writeDefaultsToo ? QString::number(a->selectedForeground().color().rgb(), 16) : QString())); settings << (p->hasProperty(QTextFormat::FontWeight) ? (p->fontBold() ? QStringLiteral("1") : QStringLiteral("0")) : (writeDefaultsToo ? (a->fontBold() ? QStringLiteral("1") : QStringLiteral("0")) : QString())); settings << (p->hasProperty(QTextFormat::FontItalic) ? (p->fontItalic() ? QStringLiteral("1") : QStringLiteral("0")) : (writeDefaultsToo ? (a->fontItalic() ? QStringLiteral("1") : QStringLiteral("0")) : QString())); settings << (p->hasProperty(QTextFormat::FontStrikeOut) ? (p->fontStrikeOut() ? QStringLiteral("1") : QStringLiteral("0")) : (writeDefaultsToo ? (a->fontStrikeOut() ? QStringLiteral("1") : QStringLiteral("0")) : QString())); settings << (p->hasProperty(QTextFormat::TextUnderlineStyle) ? (p->fontUnderline() ? QStringLiteral("1") : QStringLiteral("0")) : (writeDefaultsToo ? (a->fontUnderline() ? QStringLiteral("1") : QStringLiteral("0")) : QString())); settings << (p->hasProperty(QTextFormat::BackgroundBrush) ? QString::number(p->background().color().rgb(), 16) : ((writeDefaultsToo && a->hasProperty(QTextFormat::BackgroundBrush)) ? QString::number(a->background().color().rgb(), 16) : QString())); settings << (p->hasProperty(SelectedBackground) ? QString::number(p->selectedBackground().color().rgb(), 16) : ((writeDefaultsToo && a->hasProperty(SelectedBackground)) ? QString::number(a->selectedBackground().color().rgb(), 16) : QString())); settings << (p->hasProperty(QTextFormat::FontFamily) ? (p->fontFamily()) : (writeDefaultsToo ? a->fontFamily() : QString())); settings << QStringLiteral("---"); config.writeEntry(p->name(), settings); } } int KateHighlighting::sanitizeFormatIndex(int attrib) const { // sanitize, e.g. one could have old hl info with now invalid attribs if (attrib < 0 || size_t(attrib) >= m_formats.size()) { return 0; } return attrib; } const QHash &KateHighlighting::getCharacterEncodings(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->characterEncodings; } const KatePrefixStore &KateHighlighting::getCharacterEncodingsPrefixStore(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->characterEncodingsPrefixStore; } const QHash &KateHighlighting::getReverseCharacterEncodings(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->reverseCharacterEncodings; } bool KateHighlighting::attributeRequiresSpellchecking(int attr) { return m_formats[sanitizeFormatIndex(attr)].spellCheck(); } KTextEditor::DefaultStyle KateHighlighting::defaultStyleForAttribute(int attr) const { return textStyleToDefaultStyle(m_formats[sanitizeFormatIndex(attr)].textStyle()); } QString KateHighlighting::nameForAttrib(int attrib) const { const auto &format = m_formats.at(sanitizeFormatIndex(attrib)); - return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.name() + QLatin1Char(':') + QString(format.isValid() ? format.name() : QLatin1String("Normal")); + return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.name() + QLatin1Char(':') + QString(format.isValid() ? format.name() : QStringLiteral("Normal")); } bool KateHighlighting::isInWord(QChar c, int attrib) const { return !m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.isWordDelimiter(c) && !c.isSpace() && c != QLatin1Char('"') && c != QLatin1Char('\'') && c != QLatin1Char('`'); } bool KateHighlighting::canBreakAt(QChar c, int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.isWordWrapDelimiter(c) && c != QLatin1Char('"') && c != QLatin1Char('\''); } const QVector &KateHighlighting::emptyLines(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->emptyLines; } bool KateHighlighting::canComment(int startAttrib, int endAttrib) const { const auto startProperties = m_propertiesForFormat.at(sanitizeFormatIndex(startAttrib)); const auto endProperties = m_propertiesForFormat.at(sanitizeFormatIndex(endAttrib)); return (startProperties == endProperties && ((!startProperties->multiLineCommentStart.isEmpty() && !startProperties->multiLineCommentEnd.isEmpty()) || !startProperties->singleLineCommentMarker.isEmpty())); } QString KateHighlighting::getCommentStart(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->multiLineCommentStart; } QString KateHighlighting::getCommentEnd(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->multiLineCommentEnd; } QString KateHighlighting::getCommentSingleLineStart(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->singleLineCommentMarker; } KSyntaxHighlighting::CommentPosition KateHighlighting::getCommentSingleLinePosition(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->singleLineCommentPosition; } const QHash &KateHighlighting::characterEncodings(int attrib) const { return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->characterEncodings; } void KateHighlighting::clearAttributeArrays() { // just clear the hashed attributes, we create them lazy again m_attributeArrays.clear(); } QVector KateHighlighting::attributesForDefinition(const QString &schema) { /** * get the KSyntaxHighlighting theme from the chosen schema. * NOTE: if the theme isn't valid for KSyntaxHighlighting, an empty/invalid theme will be used. * For example, the "KDE" and "Vim (dark)" themes don't exist in KSyntaxHighlighting. */ KSyntaxHighlighting::Theme currentTheme = KateHlManager::self()->repository().theme(convertThemeName(schema)); /** * create list of all known things */ QVector array; for (const auto &format : m_formats) { /** * atm we just set the current chosen theme here for later color generation */ setTheme(currentTheme); /** * create a KTextEditor attribute matching the given format */ KTextEditor::Attribute::Ptr newAttribute(new KTextEditor::Attribute(nameForAttrib(array.size()), textStyleToDefaultStyle(format.textStyle()))); /** * NOTE: if "theme()" returns an empty theme, only the * attribute styles set in the XML files will be applied here. * FIXME: if the theme is invalid/empty, the attributes turned off in the * XML file don't work. This is so, because there the theme isn't applied by * KSyntaxHighlighting and the methods "Format::isBold(...)", "Format::isItalic(...)", * etc., return the combination between the syntax definition and the theme. */ if (format.hasTextColor(theme())) { newAttribute->setForeground(format.textColor(theme())); newAttribute->setSelectedForeground(format.selectedTextColor(theme())); } if (format.hasBackgroundColor(theme())) { newAttribute->setBackground(format.backgroundColor(theme())); newAttribute->setSelectedBackground(format.selectedBackgroundColor(theme())); } if (theme().isValid()) { if (format.isBold(theme())) { newAttribute->setFontBold(true); } else { newAttribute->setFontBold(false); } if (format.isItalic(theme())) { newAttribute->setFontItalic(true); } else { newAttribute->setFontItalic(false); } if (format.isUnderline(theme())) { newAttribute->setFontUnderline(true); } else { newAttribute->setFontUnderline(false); } if (format.isStrikeThrough(theme())) { newAttribute->setFontStrikeOut(true); } else { newAttribute->setFontStrikeOut(false); } } else { if (format.isBold(theme())) { newAttribute->setFontBold(true); } if (format.isItalic(theme())) { newAttribute->setFontItalic(true); } if (format.isUnderline(theme())) { newAttribute->setFontUnderline(true); } if (format.isStrikeThrough(theme())) { newAttribute->setFontStrikeOut(true); } } newAttribute->setSkipSpellChecking(format.spellCheck()); array.append(newAttribute); } return array; } QVector KateHighlighting::attributes(const QString &schema) { // found it, already floating around if (m_attributeArrays.contains(schema)) { return m_attributeArrays[schema]; } // k, schema correct, let create the data QVector array; KateAttributeList defaultStyleList; KateHlManager::self()->getDefaults(schema, defaultStyleList); QVector itemDataList; getKateExtendedAttributeList(schema, itemDataList); uint nAttribs = itemDataList.count(); for (uint z = 0; z < nAttribs; z++) { KTextEditor::Attribute::Ptr itemData = itemDataList.at(z); KTextEditor::Attribute::Ptr newAttribute(new KTextEditor::Attribute(*defaultStyleList.at(itemData->defaultStyle()))); if (itemData && itemData->hasAnyProperty()) { *newAttribute += *itemData; } array.append(newAttribute); } m_attributeArrays.insert(schema, array); return array; } QStringList KateHighlighting::getEmbeddedHighlightingModes() const { return embeddedHighlightingModes; } bool KateHighlighting::isEmptyLine(const Kate::TextLineData *textline) const { const QString &txt = textline->string(); if (txt.isEmpty()) { return true; } const auto &l = emptyLines(textline->attribute(0)); if (l.isEmpty()) { return false; } foreach (const QRegularExpression &re, l) { const QRegularExpressionMatch match = re.match (txt, 0, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); if (match.hasMatch() && match.capturedLength() == txt.length()) { return true; } } return false; } int KateHighlighting::attributeForLocation(KTextEditor::DocumentPrivate* doc, const KTextEditor::Cursor& cursor) { // Validate parameters to prevent out of range access if (cursor.line() < 0 || cursor.line() >= doc->lines() || cursor.column() < 0) { return 0; } // get highlighted line Kate::TextLine tl = doc->kateTextLine(cursor.line()); // make sure the textline is a valid pointer if (!tl) { return 0; } /** * either get char attribute or attribute of context still active at end of line */ if (cursor.column() < tl->length()) { return sanitizeFormatIndex(tl->attribute(cursor.column())); } else if (cursor.column() >= tl->length()) { if (!tl->attributesList().isEmpty()) { return sanitizeFormatIndex(tl->attributesList().back().attributeValue); } } return 0; } QStringList KateHighlighting::keywordsForLocation(KTextEditor::DocumentPrivate* doc, const KTextEditor::Cursor& cursor) { // FIXME-SYNTAX: was before more precise, aka context level const auto &def = m_propertiesForFormat.at(attributeForLocation(doc, cursor))->definition; QStringList keywords; for (const auto &keylist : def.keywordLists()) { keywords += def.keywordList(keylist); } return keywords; } bool KateHighlighting::spellCheckingRequiredForLocation(KTextEditor::DocumentPrivate* doc, const KTextEditor::Cursor& cursor) { return m_formats.at(attributeForLocation(doc, cursor)).spellCheck(); } QString KateHighlighting::higlightingModeForLocation(KTextEditor::DocumentPrivate* doc, const KTextEditor::Cursor& cursor) { return m_propertiesForFormat.at(attributeForLocation(doc, cursor))->definition.name(); } //END diff --git a/src/utils/codecompletionmodelcontrollerinterface.cpp b/src/utils/codecompletionmodelcontrollerinterface.cpp index 8c99a445..277989dd 100644 --- a/src/utils/codecompletionmodelcontrollerinterface.cpp +++ b/src/utils/codecompletionmodelcontrollerinterface.cpp @@ -1,131 +1,131 @@ /* This file is part of the KDE libraries Copyright (C) 2008 Niko Sams This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "codecompletionmodelcontrollerinterface.h" #include #include #include #include namespace KTextEditor { CodeCompletionModelControllerInterface::CodeCompletionModelControllerInterface() { } CodeCompletionModelControllerInterface::~CodeCompletionModelControllerInterface() { } bool CodeCompletionModelControllerInterface::shouldStartCompletion(View *view, const QString &insertedText, bool userInsertion, const Cursor &position) { Q_UNUSED(view); Q_UNUSED(position); if (insertedText.isEmpty()) { return false; } QChar lastChar = insertedText.at(insertedText.count() - 1); if ((userInsertion && (lastChar.isLetter() || lastChar.isNumber() || lastChar == QLatin1Char('_'))) || lastChar == QLatin1Char('.') || insertedText.endsWith(QLatin1String("->"))) { return true; } return false; } Range CodeCompletionModelControllerInterface::completionRange(View *view, const Cursor &position) { Cursor end = position; QString text = view->document()->line(end.line()); static const QRegularExpression findWordStart(QStringLiteral("\\b[_\\w]+$")); static const QRegularExpression findWordEnd(QStringLiteral("^[_\\w]*\\b")); Cursor start = end; int pos = text.left(end.column()).lastIndexOf(findWordStart); if (pos >= 0) { start.setColumn(pos); } QRegularExpressionMatch match; pos = text.mid(end.column()).indexOf(findWordEnd, 0, &match); if (pos >= 0) { end.setColumn(end.column() + match.capturedLength()); } return Range(start, end); } Range CodeCompletionModelControllerInterface::updateCompletionRange(View *view, const Range &range) { QStringList text = view->document()->textLines(range, false); if (!text.isEmpty() && text.count() == 1 && text.first().trimmed().isEmpty()) //When inserting a newline behind an empty completion-range,, move the range forward to its end { return Range(range.end(), range.end()); } return range; } QString CodeCompletionModelControllerInterface::filterString(View *view, const Range &range, const Cursor &position) { return view->document()->text(KTextEditor::Range(range.start(), position)); } bool CodeCompletionModelControllerInterface::shouldAbortCompletion(View *view, const Range &range, const QString ¤tCompletion) { if (view->cursorPosition() < range.start() || view->cursorPosition() > range.end()) { return true; //Always abort when the completion-range has been left } //Do not abort completions when the text has been empty already before and a newline has been entered - static const QRegularExpression allowedText(QLatin1String("^\\w*$")); + static const QRegularExpression allowedText(QStringLiteral("^\\w*$")); return !allowedText.match(currentCompletion).hasMatch(); } void CodeCompletionModelControllerInterface::aborted(KTextEditor::View *view) { Q_UNUSED(view); } bool CodeCompletionModelControllerInterface::shouldExecute(const QModelIndex &index, QChar inserted) { Q_UNUSED(index); Q_UNUSED(inserted); return false; } KTextEditor::CodeCompletionModelControllerInterface::MatchReaction CodeCompletionModelControllerInterface::matchingItem(const QModelIndex &selected) { Q_UNUSED(selected) return HideListIfAutomaticInvocation; } bool CodeCompletionModelControllerInterface::shouldHideItemsWithEqualNames() const { return false; } } diff --git a/src/utils/katecmds.cpp b/src/utils/katecmds.cpp index d23fa954..a61f86d7 100644 --- a/src/utils/katecmds.cpp +++ b/src/utils/katecmds.cpp @@ -1,597 +1,597 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2003-2005 Anders Lund * Copyright (C) 2001-2010 Christoph Cullmann * Copyright (C) 2001 Charles Samuels * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katecmds.h" #include "katedocument.h" #include "kateview.h" #include "kateautoindent.h" #include "katetextline.h" #include "katesyntaxmanager.h" #include "katerenderer.h" #include "katecmd.h" #include "katepartdebug.h" #include #include //BEGIN CoreCommands KateCommands::CoreCommands *KateCommands::CoreCommands::m_instance = nullptr; // this returns whether the string s could be converted to // a bool value, one of on|off|1|0|true|false. the argument val is // set to the extracted value in case of success static bool getBoolArg(const QString &t, bool *val) { bool res(false); QString s = t.toLower(); res = (s == QLatin1String("on") || s == QLatin1String("1") || s == QLatin1String("true")); if (res) { *val = true; return true; } res = (s == QLatin1String("off") || s == QLatin1String("0") || s == QLatin1String("false")); if (res) { *val = false; return true; } return false; } bool KateCommands::CoreCommands::help(KTextEditor::View *, const QString &cmd, QString &msg) { QString realcmd = cmd.trimmed(); if (realcmd == QLatin1String("indent")) { msg = i18n("

indent

" "

Indents the selected lines or the current line

"); return true; } else if (realcmd == QLatin1String("unindent")) { msg = i18n("

unindent

" "

Unindents the selected lines or current line.

"); return true; } else if (realcmd == QLatin1String("cleanindent")) { msg = i18n("

cleanindent

" "

Cleans up the indentation of the selected lines or current line according to the indentation settings in the document.

"); return true; } else if (realcmd == QLatin1String("comment")) { msg = i18n("

comment

" "

Inserts comment markers to make the selection or selected lines or current line a comment according to the text format as defined by the syntax highlight definition for the document.

"); return true; } else if (realcmd == QLatin1String("uncomment")) { msg = i18n("

uncomment

" "

Removes comment markers from the selection or selected lines or current line according to the text format as defined by the syntax highlight definition for the document.

"); return true; } else if (realcmd == QLatin1String("goto")) { msg = i18n("

goto line number

" "

This command navigates to the specified line number.

"); return true; } else if (realcmd == QLatin1String("set-indent-pasted-text")) { msg = i18n("

set-indent-pasted-text enable

" "

If enabled, indentation of text pasted from the clipboard is adjusted using the current indenter.

" "

Possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("kill-line")) { msg = i18n("Deletes the current line."); return true; } else if (realcmd == QLatin1String("set-tab-width")) { msg = i18n("

set-tab-width width

" "

Sets the tab width to the number width

"); return true; } else if (realcmd == QLatin1String("set-replace-tab")) { msg = i18n("

set-replace-tab enable

" "

If enabled, tabs are replaced with spaces as you type.

" "

Possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-show-tabs")) { msg = i18n("

set-show-tabs enable

" "

If enabled, TAB characters and trailing whitespace will be visualized by a small dot.

" "

Possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-remove-trailing-spaces")) { msg = i18n("

set-remove-trailing-spaces mode

" "

Removes the trailing spaces in the document depending on the mode.

" "

Possible values:" "

    " "
  • none: never remove trailing spaces.
  • " "
  • modified: remove trailing spaces only of modified lines.
  • " "
  • all: remove trailing spaces in the entire document.
  • " "

"); return true; } else if (realcmd == QLatin1String("set-indent-width")) { msg = i18n("

set-indent-width width

" "

Sets the indentation width to the number width. Used only if you are indenting with spaces.

"); return true; } else if (realcmd == QLatin1String("set-indent-mode")) { msg = i18n("

set-indent-mode mode

" "

The mode parameter is a value as seen in the Tools - Indentation menu

"); return true; } else if (realcmd == QLatin1String("set-auto-indent")) { msg = i18n("

set-auto-indent enable

" "

Enable or disable autoindentation.

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-line-numbers")) { msg = i18n("

set-line-numbers enable

" "

Sets the visibility of the line numbers pane.

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-folding-markers")) { msg = i18n("

set-folding-markers enable

" "

Sets the visibility of the folding markers pane.

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-icon-border")) { msg = i18n("

set-icon-border enable

" "

Sets the visibility of the icon border.

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-word-wrap")) { msg = i18n("

set-word-wrap enable

" "

Enables dynamic word wrap according to enable

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-word-wrap-column")) { msg = i18n("

set-word-wrap-column width

" "

Sets the line width for hard wrapping to width. This is used if you are having your text wrapped automatically.

"); return true; } else if (realcmd == QLatin1String("set-replace-tabs-save")) { msg = i18n("

set-replace-tabs-save enable

" "

When enabled, tabs will be replaced with whitespace whenever the document is saved.

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("set-highlight")) { msg = i18n("

set-highlight highlight

" "

Sets the syntax highlighting system for the document. The argument must be a valid highlight name, as seen in the Tools → Highlighting menu. This command provides an autocompletion list for its argument.

"); return true; } else if (realcmd == QLatin1String("set-mode")) { msg = i18n("

set-mode mode

" "

Sets the mode as seen in Tools - Mode

"); return true; } else if (realcmd == QLatin1String("set-show-indent")) { msg = i18n("

set-show-indent enable

" "

If enabled, indentation will be visualized by a vertical dotted line.

" "

possible true values: 1 on true
" "possible false values: 0 off false

"); return true; } else if (realcmd == QLatin1String("print")) { msg = i18n("

Open the Print dialog to print the current document.

"); return true; } else { return false; } } bool KateCommands::CoreCommands::exec(KTextEditor::View *view, const QString &_cmd, QString &errorMsg, const KTextEditor::Range &range) { #define KCC_ERR(s) { errorMsg=s; return false; } // cast it hardcore, we know that it is really a kateview :) KTextEditor::ViewPrivate *v = static_cast(view); if (! v) { KCC_ERR(i18n("Could not access view")); } //create a list of args - QStringList args(_cmd.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts)); + QStringList args(_cmd.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts)); QString cmd(args.takeFirst()); // ALL commands that takes no arguments. if (cmd == QLatin1String("indent")) { if (range.isValid()) { v->doc()->editStart(); for (int line = range.start().line(); line <= range.end().line(); line++) { v->doc()->indent(KTextEditor::Range(line, 0, line, 0), 1); } v->doc()->editEnd(); } else { v->indent(); } return true; } else if (cmd == QLatin1String("unindent")) { if (range.isValid()) { v->doc()->editStart(); for (int line = range.start().line(); line <= range.end().line(); line++) { v->doc()->indent(KTextEditor::Range(line, 0, line, 0), -1); } v->doc()->editEnd(); } else { v->unIndent(); } return true; } else if (cmd == QLatin1String("cleanindent")) { if (range.isValid()) { v->doc()->editStart(); for (int line = range.start().line(); line <= range.end().line(); line++) { v->doc()->indent(KTextEditor::Range(line, 0, line, 0), 0); } v->doc()->editEnd(); } else { v->cleanIndent(); } return true; } else if (cmd == QLatin1String("fold")) { return (v->textFolding().newFoldingRange(range.isValid() ? range : v->selectionRange(), Kate::TextFolding::Persistent | Kate::TextFolding::Folded) != -1); } else if (cmd == QLatin1String("tfold")) { return (v->textFolding().newFoldingRange(range.isValid() ? range : v->selectionRange(), Kate::TextFolding::Folded) != -1); } else if (cmd == QLatin1String("unfold")) { QVector > startingRanges = v->textFolding().foldingRangesStartingOnLine(v->cursorPosition().line()); bool unfolded = false; for (int i = 0; i < startingRanges.size(); ++i) { if (startingRanges[i].second & Kate::TextFolding::Folded) { unfolded = v->textFolding().unfoldRange(startingRanges[i].first) || unfolded; } } return unfolded; } else if (cmd == QLatin1String("comment")) { if (range.isValid()) { v->doc()->editStart(); for (int line = range.start().line(); line <= range.end().line(); line++) { v->doc()->comment(v, line, 0, 1); } v->doc()->editEnd(); } else { v->comment(); } return true; } else if (cmd == QLatin1String("uncomment")) { if (range.isValid()) { v->doc()->editStart(); for (int line = range.start().line(); line <= range.end().line(); line++) { v->doc()->comment(v, line, 0, -1); } v->doc()->editEnd(); } else { v->uncomment(); } return true; } else if (cmd == QLatin1String("kill-line")) { if (range.isValid()) { v->doc()->editStart(); for (int line = range.start().line(); line <= range.end().line(); line++) { v->doc()->removeLine(range.start().line()); } v->doc()->editEnd(); } else { v->killLine(); } return true; } else if (cmd == QLatin1String("print")) { v->print(); return true; } // ALL commands that take a string argument else if (cmd == QLatin1String("set-indent-mode") || cmd == QLatin1String("set-highlight") || cmd == QLatin1String("set-mode")) { // need at least one item, otherwise args.first() crashes if (args.isEmpty()) { KCC_ERR(i18n("Missing argument. Usage: %1 ", cmd)); } if (cmd == QLatin1String("set-indent-mode")) { v->doc()->config()->setIndentationMode(args.join(QLatin1Char(' '))); v->doc()->rememberUserDidSetIndentationMode(); return true; } else if (cmd == QLatin1String("set-highlight")) { if (v->doc()->setHighlightingMode(args.join(QLatin1Char(' ')))) { static_cast(v->doc())->setDontChangeHlOnSave(); return true; } KCC_ERR(i18n("No such highlighting '%1'", args.first())); } else if (cmd == QLatin1String("set-mode")) { if (v->doc()->setMode(args.first())) { return true; } KCC_ERR(i18n("No such mode '%1'", args.first())); } } // ALL commands that takes exactly one integer argument. else if (cmd == QLatin1String("set-tab-width") || cmd == QLatin1String("set-indent-width") || cmd == QLatin1String("set-word-wrap-column") || cmd == QLatin1String("goto")) { // find a integer value > 0 if (args.isEmpty()) { KCC_ERR(i18n("Missing argument. Usage: %1 ", cmd)); } bool ok; int val(args.first().toInt(&ok, 10)); // use base 10 even if the string starts with '0' if (!ok) KCC_ERR(i18n("Failed to convert argument '%1' to integer.", args.first())); if (cmd == QLatin1String("set-tab-width")) { if (val < 1) { KCC_ERR(i18n("Width must be at least 1.")); } v->doc()->config()->setTabWidth(val); } else if (cmd == QLatin1String("set-indent-width")) { if (val < 1) { KCC_ERR(i18n("Width must be at least 1.")); } v->doc()->config()->setIndentationWidth(val); } else if (cmd == QLatin1String("set-word-wrap-column")) { if (val < 2) { KCC_ERR(i18n("Column must be at least 1.")); } v->doc()->setWordWrapAt(val); } else if (cmd == QLatin1String("goto")) { if (args.first().at(0) == QLatin1Char('-') || args.first().at(0) == QLatin1Char('+')) { // if the number starts with a minus or plus sign, add/subtract the number val = v->cursorPosition().line() + val; } else { val--; // convert given line number to the internal representation of line numbers } // constrain cursor to the range [0, number of lines] if (val < 0) { val = 0; } else if (val > v->doc()->lines() - 1) { val = v->doc()->lines() - 1; } v->setCursorPosition(KTextEditor::Cursor(val, 0)); return true; } return true; } // ALL commands that takes 1 boolean argument. else if (cmd == QLatin1String("set-icon-border") || cmd == QLatin1String("set-folding-markers") || cmd == QLatin1String("set-indent-pasted-text") || cmd == QLatin1String("set-line-numbers") || cmd == QLatin1String("set-replace-tabs") || cmd == QLatin1String("set-show-tabs") || cmd == QLatin1String("set-word-wrap") || cmd == QLatin1String("set-wrap-cursor") || cmd == QLatin1String("set-replace-tabs-save") || cmd == QLatin1String("set-show-indent")) { if (args.isEmpty()) { KCC_ERR(i18n("Usage: %1 on|off|1|0|true|false", cmd)); } bool enable = false; KateDocumentConfig *const config = v->doc()->config(); if (getBoolArg(args.first(), &enable)) { if (cmd == QLatin1String("set-icon-border")) { v->setIconBorder(enable); } else if (cmd == QLatin1String("set-folding-markers")) { v->setFoldingMarkersOn(enable); } else if (cmd == QLatin1String("set-line-numbers")) { v->setLineNumbersOn(enable); } else if (cmd == QLatin1String("set-show-indent")) { v->renderer()->setShowIndentLines(enable); } else if (cmd == QLatin1String("set-indent-pasted-text")) { config->setIndentPastedText(enable); } else if (cmd == QLatin1String("set-replace-tabs")) { config->setReplaceTabsDyn(enable); } else if (cmd == QLatin1String("set-show-tabs")) { config->setShowTabs(enable); } else if (cmd == QLatin1String("set-show-trailing-spaces")) { config->setShowSpaces(enable ? KateDocumentConfig::Trailing : KateDocumentConfig::None); } else if (cmd == QLatin1String("set-word-wrap")) { v->doc()->setWordWrap(enable); } return true; } else KCC_ERR(i18n("Bad argument '%1'. Usage: %2 on|off|1|0|true|false", args.first(), cmd)); } else if (cmd == QLatin1String("set-remove-trailing-spaces")) { // need at least one item, otherwise args.first() crashes if (args.count() != 1) { KCC_ERR(i18n("Usage: set-remove-trailing-spaces 0|-|none or 1|+|mod|modified or 2|*|all")); } QString tmp = args.first().toLower().trimmed(); if (tmp == QLatin1String("1") || tmp == QLatin1String("modified") || tmp == QLatin1String("mod") || tmp == QLatin1String("+")) { v->doc()->config()->setRemoveSpaces(1); } else if (tmp == QLatin1String("2") || tmp == QLatin1String("all") || tmp == QLatin1String("*")) { v->doc()->config()->setRemoveSpaces(2); } else { v->doc()->config()->setRemoveSpaces(0); } } // unlikely.. KCC_ERR(i18n("Unknown command '%1'", cmd)); } bool KateCommands::CoreCommands::supportsRange(const QString &range) { static QStringList l; if (l.isEmpty()) l << QStringLiteral("indent") << QStringLiteral("unindent") << QStringLiteral("cleanindent") << QStringLiteral("comment") << QStringLiteral("uncomment") << QStringLiteral("kill-line") << QStringLiteral("fold") << QStringLiteral("tfold"); return l.contains(range); } KCompletion *KateCommands::CoreCommands::completionObject(KTextEditor::View *view, const QString &cmd) { Q_UNUSED(view) if (cmd == QLatin1String("set-highlight")) { QStringList l; for (const auto &hl : KateHlManager::self()->modeList()) { l << hl.name(); } KateCmdShellCompletion *co = new KateCmdShellCompletion(); co->setItems(l); co->setIgnoreCase(true); return co; } else if (cmd == QLatin1String("set-remove-trailing-spaces")) { QStringList l; l << QStringLiteral("none") << QStringLiteral("modified") << QStringLiteral("all"); KateCmdShellCompletion *co = new KateCmdShellCompletion(); co->setItems(l); co->setIgnoreCase(true); return co; } else if (cmd == QLatin1String("set-indent-mode")) { QStringList l = KateAutoIndent::listIdentifiers(); KateCmdShellCompletion *co = new KateCmdShellCompletion(); co->setItems(l); co->setIgnoreCase(true); return co; } return nullptr; } //END CoreCommands //BEGIN Character KateCommands::Character *KateCommands::Character::m_instance = nullptr; bool KateCommands::Character::help(class KTextEditor::View *, const QString &cmd, QString &msg) { if (cmd.trimmed() == QLatin1String("char")) { msg = i18n("

char identifier

" "

This command allows you to insert literal characters by their numerical identifier, in decimal, octal or hexadecimal form.

" "

Examples:

    " "
  • char 234
  • " "
  • char 0x1234
  • " "

"); return true; } return false; } bool KateCommands::Character::exec(KTextEditor::View *view, const QString &_cmd, QString &, const KTextEditor::Range &) { QString cmd = _cmd; // hex, octal, base 9+1 - QRegularExpression num(QLatin1String("^char *(0?x[0-9A-Fa-f]{1,4}|0[0-7]{1,6}|[0-9]{1,5})$")); + QRegularExpression num(QStringLiteral("^char *(0?x[0-9A-Fa-f]{1,4}|0[0-7]{1,6}|[0-9]{1,5})$")); QRegularExpressionMatch match = num.match(cmd); if (!match.hasMatch()) { return false; } cmd = match.captured(1); // identify the base unsigned short int number = 0; int base = 10; if (cmd.startsWith(QLatin1Char('x'))) { cmd.remove(0, 1); base = 16; } else if (cmd.startsWith(QLatin1String("0x"))) { cmd.remove(0, 2); base = 16; } else if (cmd[0] == QLatin1Char('0')) { base = 8; } bool ok; number = cmd.toUShort(&ok, base); if (!ok || number == 0) { return false; } if (number <= 255) { char buf[2]; buf[0] = (char)number; buf[1] = 0; view->document()->insertText(view->cursorPosition(), QString::fromLatin1(buf)); } else { // do the unicode thing QChar c(number); view->document()->insertText(view->cursorPosition(), QString(&c, 1)); } return true; } //END Character //BEGIN Date KateCommands::Date *KateCommands::Date::m_instance = nullptr; bool KateCommands::Date::help(class KTextEditor::View *, const QString &cmd, QString &msg) { if (cmd.trimmed() == QLatin1String("date")) { msg = i18n("

date or date format

" "

Inserts a date/time string as defined by the specified format, or the format yyyy-MM-dd hh:mm:ss if none is specified.

" "

Possible format specifiers are:" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
dThe day as number without a leading zero (1-31).
ddThe day as number with a leading zero (01-31).
dddThe abbreviated localized day name (e.g. 'Mon'..'Sun').
ddddThe long localized day name (e.g. 'Monday'..'Sunday').
MThe month as number without a leading zero (1-12).
MMThe month as number with a leading zero (01-12).
MMMThe abbreviated localized month name (e.g. 'Jan'..'Dec').
yyThe year as two digit number (00-99).
yyyyThe year as four digit number (1752-8000).
hThe hour without a leading zero (0..23 or 1..12 if AM/PM display).
hhThe hour with a leading zero (00..23 or 01..12 if AM/PM display).
mThe minute without a leading zero (0..59).
mmThe minute with a leading zero (00..59).
sThe second without a leading zero (0..59).
ssThe second with a leading zero (00..59).
zThe milliseconds without leading zeroes (0..999).
zzzThe milliseconds with leading zeroes (000..999).
APUse AM/PM display. AP will be replaced by either \"AM\" or \"PM\".
apUse am/pm display. ap will be replaced by either \"am\" or \"pm\".

"); return true; } return false; } bool KateCommands::Date::exec(KTextEditor::View *view, const QString &cmd, QString &, const KTextEditor::Range &) { if (!cmd.startsWith(QLatin1String("date"))) { return false; } if (QDateTime::currentDateTime().toString(cmd.mid(5, cmd.length() - 5)).length() > 0) { view->document()->insertText(view->cursorPosition(), QDateTime::currentDateTime().toString(cmd.mid(5, cmd.length() - 5))); } else { view->document()->insertText(view->cursorPosition(), QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))); } return true; } //END Date diff --git a/src/utils/katesedcmd.cpp b/src/utils/katesedcmd.cpp index af417fc9..a0f1bfd8 100644 --- a/src/utils/katesedcmd.cpp +++ b/src/utils/katesedcmd.cpp @@ -1,304 +1,304 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2003-2005 Anders Lund * Copyright (C) 2001-2010 Christoph Cullmann * Copyright (C) 2001 Charles Samuels * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katesedcmd.h" #include "katedocument.h" #include "kateview.h" #include "kateglobal.h" #include "katecmd.h" #include "katepartdebug.h" #include #include #include #include KateCommands::SedReplace *KateCommands::SedReplace::m_instance = nullptr; static int backslashString(const QString &haystack, const QString &needle, int index) { int len = haystack.length(); int searchlen = needle.length(); bool evenCount = true; while (index < len) { if (haystack[index] == QLatin1Char('\\')) { evenCount = !evenCount; } else { // isn't a slash if (!evenCount) { if (haystack.mid(index, searchlen) == needle) { return index - 1; } } evenCount = true; } ++index; } return -1; } // exchange "\t" for the actual tab character, for example static void exchangeAbbrevs(QString &str) { // the format is (findreplace)*[nullzero] const char *magic = "a\x07t\tn\n"; while (*magic) { int index = 0; char replace = magic[1]; while ((index = backslashString(str, QString(QChar::fromLatin1(*magic)), index)) != -1) { str.replace(index, 2, QChar::fromLatin1(replace)); ++index; } ++magic; ++magic; } } bool KateCommands::SedReplace::exec(class KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &r) { qCDebug(LOG_KTE) << "SedReplace::execCmd( " << cmd << " )"; if (r.isValid()) { qCDebug(LOG_KTE) << "Range: " << r; } int findBeginPos = -1; int findEndPos = -1; int replaceBeginPos = -1; int replaceEndPos = -1; QString delimiter; if (!parse(cmd, delimiter, findBeginPos, findEndPos, replaceBeginPos, replaceEndPos)) { return false; } const QString searchParamsString = cmd.mid(cmd.lastIndexOf(delimiter)); const bool noCase = searchParamsString.contains(QLatin1Char('i')); const bool repeat = searchParamsString.contains(QLatin1Char('g')); const bool interactive = searchParamsString.contains(QLatin1Char('c')); QString find = cmd.mid(findBeginPos, findEndPos - findBeginPos + 1); qCDebug(LOG_KTE) << "SedReplace: find =" << find; QString replace = cmd.mid(replaceBeginPos, replaceEndPos - replaceBeginPos + 1); exchangeAbbrevs(replace); qCDebug(LOG_KTE) << "SedReplace: replace =" << replace; if (find.isEmpty()) { // Nothing to do. return true; } KTextEditor::ViewPrivate *kateView = static_cast(view); KTextEditor::DocumentPrivate *doc = kateView->doc(); if (!doc) { return false; } // Only current line ... int startLine = kateView->cursorPosition().line(); int endLine = kateView->cursorPosition().line(); // ... unless a range was provided. if (r.isValid()) { startLine = r.start().line(); endLine = r.end().line(); } QSharedPointer interactiveSedReplacer(new InteractiveSedReplacer(doc, find, replace, !noCase, !repeat, startLine, endLine)); if (interactive) { const bool hasInitialMatch = interactiveSedReplacer->currentMatch().isValid(); if (!hasInitialMatch) { // Can't start an interactive sed replace if there is no initial match! msg = interactiveSedReplacer->finalStatusReportMessage(); return false; } interactiveSedReplace(kateView, interactiveSedReplacer); return true; } interactiveSedReplacer->replaceAllRemaining(); msg = interactiveSedReplacer->finalStatusReportMessage(); return true; } bool KateCommands::SedReplace::interactiveSedReplace(KTextEditor::ViewPrivate *, QSharedPointer) { qCDebug(LOG_KTE) << "Interactive sedreplace is only currently supported with Vi mode plus Vi emulated command bar."; return false; } bool KateCommands::SedReplace::parse(const QString &sedReplaceString, QString &destDelim, int &destFindBeginPos, int &destFindEndPos, int &destReplaceBeginPos, int &destReplaceEndPos) { // valid delimiters are all non-word, non-space characters plus '_' - QRegularExpression delim(QLatin1String("^s\\s*([^\\w\\s]|_)")); + QRegularExpression delim(QStringLiteral("^s\\s*([^\\w\\s]|_)")); auto match = delim.match(sedReplaceString); if (!match.hasMatch()) { return false; } const QString d = match.captured(1); qCDebug(LOG_KTE) << "SedReplace: delimiter is '" << d << "'"; QRegularExpression splitter(QStringLiteral("^s\\s*") + d + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)\\") + d + QLatin1String("((?:[^\\\\\\") + d + QLatin1String("]|\\\\.)*)(\\") + d + QLatin1String("[igc]{0,3})?$")); match = splitter.match(sedReplaceString); if (!match.hasMatch()) { return false; } const QString find = match.captured(1); const QString replace = match.captured(2); destDelim = d; destFindBeginPos = match.capturedStart(1); destFindEndPos = match.capturedStart(1) + find.length() - 1; destReplaceBeginPos = match.capturedStart(2); destReplaceEndPos = match.capturedStart(2) + replace.length() - 1; return true; } KateCommands::SedReplace::InteractiveSedReplacer::InteractiveSedReplacer(KTextEditor::DocumentPrivate *doc, const QString &findPattern, const QString &replacePattern, bool caseSensitive, bool onlyOnePerLine, int startLine, int endLine) : m_findPattern(findPattern), m_replacePattern(replacePattern), m_onlyOnePerLine(onlyOnePerLine), m_endLine(endLine), m_doc(doc), m_regExpSearch(doc, caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive), m_numReplacementsDone(0), m_numLinesTouched(0), m_lastChangedLineNum(-1) { m_currentSearchPos = KTextEditor::Cursor(startLine, 0); } KTextEditor::Range KateCommands::SedReplace::InteractiveSedReplacer::currentMatch() { QVector matches = fullCurrentMatch(); if (matches.isEmpty()) { return KTextEditor::Range::invalid(); } if (matches.first().start().line() > m_endLine) { return KTextEditor::Range::invalid(); } return matches.first(); } void KateCommands::SedReplace::InteractiveSedReplacer::skipCurrentMatch() { const KTextEditor::Range currentMatch = this->currentMatch(); m_currentSearchPos = currentMatch.end(); if (m_onlyOnePerLine && currentMatch.start().line() == m_currentSearchPos.line()) { m_currentSearchPos = KTextEditor::Cursor(m_currentSearchPos.line() + 1, 0); } } void KateCommands::SedReplace::InteractiveSedReplacer::replaceCurrentMatch() { const KTextEditor::Range currentMatch = this->currentMatch(); const QString currentMatchText = m_doc->text(currentMatch); const QString replacementText = replacementTextForCurrentMatch(); m_doc->editBegin(); m_doc->removeText(currentMatch); m_doc->insertText(currentMatch.start(), replacementText); m_doc->editEnd(); // Begin next search from directly after replacement. if (!replacementText.contains(QLatin1Char('\n'))) { const int moveChar = currentMatch.isEmpty() ? 1 : 0; // if the search was for \s*, make sure we advance a char const int col = currentMatch.start().column() + replacementText.length() + moveChar; m_currentSearchPos = KTextEditor::Cursor(currentMatch.start().line(), col); } else { m_currentSearchPos = KTextEditor::Cursor(currentMatch.start().line() + replacementText.count(QLatin1Char('\n')), replacementText.length() - replacementText.lastIndexOf(QLatin1Char('\n')) - 1); } if (m_onlyOnePerLine) { // Drop down to next line. m_currentSearchPos = KTextEditor::Cursor(m_currentSearchPos.line() + 1, 0); } // Adjust end line down by the number of new newlines just added, minus the number taken away. m_endLine += replacementText.count(QLatin1Char('\n')); m_endLine -= currentMatchText.count(QLatin1Char('\n')); m_numReplacementsDone++; if (m_lastChangedLineNum != currentMatch.start().line()) { // Counting "swallowed" lines as being "touched". m_numLinesTouched += currentMatchText.count(QLatin1Char('\n')) + 1; } m_lastChangedLineNum = m_currentSearchPos.line(); } void KateCommands::SedReplace::InteractiveSedReplacer::replaceAllRemaining() { m_doc->editBegin(); while (currentMatch().isValid()) { replaceCurrentMatch(); } m_doc->editEnd(); } QString KateCommands::SedReplace::InteractiveSedReplacer::currentMatchReplacementConfirmationMessage() { return i18n("replace with %1?", replacementTextForCurrentMatch().replace(QLatin1Char('\n'), QLatin1String("\\n"))); } QString KateCommands::SedReplace::InteractiveSedReplacer::finalStatusReportMessage() { return i18ncp("%2 is the translation of the next message", "1 replacement done on %2", "%1 replacements done on %2", m_numReplacementsDone, i18ncp("substituted into the previous message", "1 line", "%1 lines", m_numLinesTouched)); } const QVector KateCommands::SedReplace::InteractiveSedReplacer::fullCurrentMatch() { if (m_currentSearchPos > m_doc->documentEnd()) { return QVector (); } return m_regExpSearch.search(m_findPattern, KTextEditor::Range(m_currentSearchPos, m_doc->documentEnd())); } QString KateCommands::SedReplace::InteractiveSedReplacer::replacementTextForCurrentMatch() { const QVector captureRanges = fullCurrentMatch(); QStringList captureTexts; foreach (KTextEditor::Range captureRange, captureRanges) { captureTexts << m_doc->text(captureRange); } const QString replacementText = m_regExpSearch.buildReplacement(m_replacePattern, captureTexts, 0); return replacementText; } diff --git a/src/view/kateview.cpp b/src/view/kateview.cpp index 097c3e54..48dda1cb 100644 --- a/src/view/kateview.cpp +++ b/src/view/kateview.cpp @@ -1,3960 +1,3960 @@ /* This file is part of the KDE libraries Copyright (C) 2009 Michel Ludwig Copyright (C) 2007 Mirko Stocker Copyright (C) 2003 Hamish Rodda Copyright (C) 2002 John Firebaugh Copyright (C) 2001-2004 Christoph Cullmann Copyright (C) 2001-2010 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //BEGIN includes #include "kateview.h" #include "kateviewinternal.h" #include "kateviewhelpers.h" #include "katerenderer.h" #include "katedocument.h" #include "kateundomanager.h" #include "kateglobal.h" #include "katehighlight.h" #include "katehighlightmenu.h" #include "katedialogs.h" #include "katetextline.h" #include "kateschema.h" #include "katebookmarks.h" #include "kateconfig.h" #include "katemodemenu.h" #include "kateautoindent.h" #include "katecompletionwidget.h" #include "katewordcompletion.h" #include "katekeywordcompletion.h" #include "katelayoutcache.h" #include "spellcheck/spellcheck.h" #include "spellcheck/spellcheckdialog.h" #include "spellcheck/spellingmenu.h" #include "katebuffer.h" #include "script/katescriptmanager.h" #include "script/katescriptaction.h" #include "export/exporter.h" #include "katemessagewidget.h" #include "katetemplatehandler.h" #include "katepartdebug.h" #include "printing/kateprinter.h" #include "katestatusbar.h" #include "kateabstractinputmode.h" #include "inlinenotedata.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#define VIEW_RANGE_DEBUG //END includes namespace { bool hasCommentInFirstLine(KTextEditor::DocumentPrivate* doc) { const Kate::TextLine& line = doc->kateTextLine(0); Q_ASSERT(line); return doc->isComment(0, line->firstChar()); } } void KTextEditor::ViewPrivate::blockFix(KTextEditor::Range &range) { if (range.start().column() > range.end().column()) { int tmp = range.start().column(); range.setStart(KTextEditor::Cursor(range.start().line(), range.end().column())); range.setEnd(KTextEditor::Cursor(range.end().line(), tmp)); } } KTextEditor::ViewPrivate::ViewPrivate(KTextEditor::DocumentPrivate *doc, QWidget *parent, KTextEditor::MainWindow *mainWindow) : KTextEditor::View (this, parent) , m_completionWidget(nullptr) , m_annotationModel(nullptr) , m_hasWrap(false) , m_doc(doc) , m_textFolding(doc->buffer()) , m_config(new KateViewConfig(this)) , m_renderer(new KateRenderer(doc, m_textFolding, this)) , m_viewInternal(new KateViewInternal(this)) , m_spell(new KateSpellCheckDialog(this)) , m_bookmarks(new KateBookmarks(this)) , m_topSpacer(new QSpacerItem(0,0)) , m_leftSpacer(new QSpacerItem(0,0)) , m_rightSpacer(new QSpacerItem(0,0)) , m_bottomSpacer(new QSpacerItem(0,0)) , m_startingUp(true) , m_updatingDocumentConfig(false) , m_selection(m_doc->buffer(), KTextEditor::Range::invalid(), Kate::TextRange::ExpandLeft, Kate::TextRange::AllowEmpty) , blockSelect(false) , m_bottomViewBar(nullptr) , m_gotoBar(nullptr) , m_dictionaryBar(nullptr) , m_spellingMenu(new KateSpellingMenu(this)) , m_userContextMenuSet(false) , m_delayedUpdateTriggered(false) , m_lineToUpdateMin(-1) , m_lineToUpdateMax(-1) , m_mainWindow(mainWindow ? mainWindow : KTextEditor::EditorPrivate::self()->dummyMainWindow()) // use dummy window if no window there! , m_statusBar(nullptr) , m_temporaryAutomaticInvocationDisabled(false) , m_autoFoldedFirstLine(false) { // queued connect to collapse view updates for range changes, INIT THIS EARLY ENOUGH! connect(this, SIGNAL(delayedUpdateOfView()), this, SLOT(slotDelayedUpdateOfView()), Qt::QueuedConnection); KXMLGUIClient::setComponentName(KTextEditor::EditorPrivate::self()->aboutData().componentName(), KTextEditor::EditorPrivate::self()->aboutData().displayName()); // selection if for this view only and will invalidate if becoming empty m_selection.setView(this); // use z depth defined in moving ranges interface m_selection.setZDepth(-100000.0); KTextEditor::EditorPrivate::self()->registerView(this); /** * try to let the main window, if any, create a view bar for this view */ QWidget *bottomBarParent = m_mainWindow->createViewBar(this); m_bottomViewBar = new KateViewBar(bottomBarParent != nullptr, bottomBarParent ? bottomBarParent : this, this); // ugly workaround: // Force the layout to be left-to-right even on RTL desktop, as discussed // on the mailing list. This will cause the lines and icons panel to be on // the left, even for Arabic/Hebrew/Farsi/whatever users. setLayoutDirection(Qt::LeftToRight); m_bottomViewBar->installEventFilter(m_viewInternal); // add KateMessageWidget for KTE::MessageInterface immediately above view m_messageWidgets[KTextEditor::Message::AboveView] = new KateMessageWidget(this); m_messageWidgets[KTextEditor::Message::AboveView]->hide(); // add KateMessageWidget for KTE::MessageInterface immediately above view m_messageWidgets[KTextEditor::Message::BelowView] = new KateMessageWidget(this); m_messageWidgets[KTextEditor::Message::BelowView]->hide(); // add bottom viewbar... if (bottomBarParent) { m_mainWindow->addWidgetToViewBar(this, m_bottomViewBar); } // add layout for floating message widgets to KateViewInternal m_notificationLayout = new KateMessageLayout(m_viewInternal); m_notificationLayout->setContentsMargins(20, 20, 20, 20); m_viewInternal->setLayout(m_notificationLayout); // this really is needed :) m_viewInternal->updateView(); doc->addView(this); setFocusProxy(m_viewInternal); setFocusPolicy(Qt::StrongFocus); setXMLFile(QStringLiteral("katepart5ui.rc")); setupConnections(); setupActions(); // auto word completion new KateWordCompletionView(this, actionCollection()); // update the enabled state of the undo/redo actions... slotUpdateUndo(); /** * create the status bar of this view * do this after action creation, we use some of them! */ toggleStatusBar(); m_startingUp = false; updateConfig(); slotHlChanged(); KCursor::setAutoHideCursor(m_viewInternal, true); for (auto messageWidget : m_messageWidgets) { if (messageWidget) { // user interaction (scrolling) starts notification auto-hide timer connect(this, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate*)), messageWidget, SLOT(startAutoHideTimer())); // user interaction (cursor navigation) starts notification auto-hide timer connect(this, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), messageWidget, SLOT(startAutoHideTimer())); } } // folding restoration on reload connect(m_doc, SIGNAL(aboutToReload(KTextEditor::Document*)), SLOT(saveFoldingState())); connect(m_doc, SIGNAL(reloaded(KTextEditor::Document*)), SLOT(applyFoldingState())); connect(m_doc, &KTextEditor::DocumentPrivate::reloaded, this, &KTextEditor::ViewPrivate::slotDocumentReloaded); connect(m_doc, &KTextEditor::DocumentPrivate::aboutToReload, this, &KTextEditor::ViewPrivate::slotDocumentAboutToReload); // update highlights on scrolling and co connect(this, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate*)), this, SLOT(createHighlights())); // clear highlights on reload connect(m_doc, SIGNAL(aboutToReload(KTextEditor::Document*)), SLOT(clearHighlights())); // setup layout setupLayout(); } KTextEditor::ViewPrivate::~ViewPrivate() { // invalidate update signal m_delayedUpdateTriggered = false; // remove from xmlgui factory, to be safe if (factory()) { factory()->removeClient(this); } // delete internal view before view bar! delete m_viewInternal; /** * remove view bar again, if needed */ m_mainWindow->deleteViewBar(this); m_bottomViewBar = nullptr; doc()->removeView(this); delete m_renderer; delete m_config; KTextEditor::EditorPrivate::self()->deregisterView(this); } void KTextEditor::ViewPrivate::toggleStatusBar() { /** * if there, delete it */ if (m_statusBar) { bottomViewBar()->removePermanentBarWidget(m_statusBar); delete m_statusBar; m_statusBar = nullptr; emit statusBarEnabledChanged(this, false); return; } /** * else: create it */ m_statusBar = new KateStatusBar(this); bottomViewBar()->addPermanentBarWidget(m_statusBar); emit statusBarEnabledChanged(this, true); } void KTextEditor::ViewPrivate::setupLayout() { // delete old layout if any if (layout()) { delete layout(); /** * need to recreate spacers because they are deleted with * their belonging layout */ m_topSpacer = new QSpacerItem(0,0); m_leftSpacer = new QSpacerItem(0,0); m_rightSpacer = new QSpacerItem(0,0); m_bottomSpacer = new QSpacerItem(0,0); } // set margins QStyleOptionFrame opt; opt.initFrom(this); opt.frameShape = QFrame::StyledPanel; opt.state |= QStyle::State_Sunken; const int margin = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, this); m_topSpacer->changeSize(0, margin, QSizePolicy::Minimum, QSizePolicy::Fixed); m_leftSpacer->changeSize(margin, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); m_rightSpacer->changeSize(margin, 0, QSizePolicy::Fixed, QSizePolicy::Minimum); m_bottomSpacer->changeSize(0, margin, QSizePolicy::Minimum, QSizePolicy::Fixed); // define layout QGridLayout *layout=new QGridLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); const bool frameAroundContents = style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &opt, this); if (frameAroundContents) { // top message widget layout->addWidget(m_messageWidgets[KTextEditor::Message::AboveView], 0, 0, 1, 5); // top spacer layout->addItem(m_topSpacer, 1, 0, 1, 4); // left spacer layout->addItem(m_leftSpacer, 2, 0, 1, 1); // left border layout->addWidget(m_viewInternal->m_leftBorder, 2, 1, 1, 1); // view layout->addWidget(m_viewInternal, 2, 2, 1, 1); // right spacer layout->addItem(m_rightSpacer, 2, 3, 1, 1); // bottom spacer layout->addItem(m_bottomSpacer, 3, 0, 1, 4); // vertical scrollbar layout->addWidget(m_viewInternal->m_lineScroll, 1, 4, 3, 1); // horizontal scrollbar layout->addWidget(m_viewInternal->m_columnScroll, 4, 0, 1, 4); // dummy layout->addWidget(m_viewInternal->m_dummy, 4, 4, 1, 1); // bottom message layout->addWidget(m_messageWidgets[KTextEditor::Message::BelowView], 5, 0, 1, 5); // bottom viewbar if (m_bottomViewBar->parentWidget() == this) { layout->addWidget(m_bottomViewBar, 6, 0, 1, 5); } // stretch layout->setColumnStretch(2, 1); layout->setRowStretch(2, 1); // adjust scrollbar background m_viewInternal->m_lineScroll->setBackgroundRole(QPalette::Window); m_viewInternal->m_lineScroll->setAutoFillBackground(false); m_viewInternal->m_columnScroll->setBackgroundRole(QPalette::Window); m_viewInternal->m_columnScroll->setAutoFillBackground(false); } else { // top message widget layout->addWidget(m_messageWidgets[KTextEditor::Message::AboveView], 0, 0, 1, 5); // top spacer layout->addItem(m_topSpacer, 1, 0, 1, 5); // left spacer layout->addItem(m_leftSpacer, 2, 0, 1, 1); // left border layout->addWidget(m_viewInternal->m_leftBorder, 2, 1, 1, 1); // view layout->addWidget(m_viewInternal, 2, 2, 1, 1); // vertical scrollbar layout->addWidget(m_viewInternal->m_lineScroll, 2, 3, 1, 1); // right spacer layout->addItem(m_rightSpacer, 2, 4, 1, 1); // horizontal scrollbar layout->addWidget(m_viewInternal->m_columnScroll, 3, 1, 1, 2); // dummy layout->addWidget(m_viewInternal->m_dummy, 3, 3, 1, 1); // bottom spacer layout->addItem(m_bottomSpacer, 4, 0, 1, 5); // bottom message layout->addWidget(m_messageWidgets[KTextEditor::Message::BelowView], 5, 0, 1, 5); // bottom viewbar if (m_bottomViewBar->parentWidget() == this) { layout->addWidget(m_bottomViewBar, 6, 0, 1, 5); } // stretch layout->setColumnStretch(2, 1); layout->setRowStretch(2, 1); // adjust scrollbar background m_viewInternal->m_lineScroll->setBackgroundRole(QPalette::Base); m_viewInternal->m_lineScroll->setAutoFillBackground(true); m_viewInternal->m_columnScroll->setBackgroundRole(QPalette::Base); m_viewInternal->m_columnScroll->setAutoFillBackground(true); } } void KTextEditor::ViewPrivate::setupConnections() { connect(m_doc, SIGNAL(undoChanged()), this, SLOT(slotUpdateUndo())); connect(m_doc, SIGNAL(highlightingModeChanged(KTextEditor::Document*)), this, SLOT(slotHlChanged())); connect(m_doc, SIGNAL(canceled(QString)), this, SLOT(slotSaveCanceled(QString))); connect(m_viewInternal, SIGNAL(dropEventPass(QDropEvent*)), this, SIGNAL(dropEventPass(QDropEvent*))); connect(m_doc, SIGNAL(annotationModelChanged(KTextEditor::AnnotationModel*,KTextEditor::AnnotationModel*)), m_viewInternal->m_leftBorder, SLOT(annotationModelChanged(KTextEditor::AnnotationModel*,KTextEditor::AnnotationModel*))); } void KTextEditor::ViewPrivate::goToPreviousEditingPosition() { auto c = doc()->lastEditingPosition(KTextEditor::DocumentPrivate::Previous, cursorPosition()); if(c.isValid()){ setCursorPosition(c); } } void KTextEditor::ViewPrivate::goToNextEditingPosition() { auto c = doc()->lastEditingPosition(KTextEditor::DocumentPrivate::Next, cursorPosition()); if(c.isValid()){ setCursorPosition(c); } } void KTextEditor::ViewPrivate::setupActions() { KActionCollection *ac = actionCollection(); QAction *a; m_toggleWriteLock = nullptr; m_cut = a = ac->addAction(KStandardAction::Cut, this, SLOT(cut())); a->setWhatsThis(i18n("Cut the selected text and move it to the clipboard")); m_paste = a = ac->addAction(KStandardAction::Paste, this, SLOT(paste())); a->setWhatsThis(i18n("Paste previously copied or cut clipboard contents")); m_copy = a = ac->addAction(KStandardAction::Copy, this, SLOT(copy())); a->setWhatsThis(i18n("Use this command to copy the currently selected text to the system clipboard.")); m_pasteMenu = ac->addAction(QStringLiteral("edit_paste_menu"), new KatePasteMenu(i18n("Clipboard &History"), this)); connect(KTextEditor::EditorPrivate::self(), SIGNAL(clipboardHistoryChanged()), this, SLOT(slotClipboardHistoryChanged())); if (!doc()->readOnly()) { a = ac->addAction(KStandardAction::Save, m_doc, SLOT(documentSave())); a->setWhatsThis(i18n("Save the current document")); a = m_editUndo = ac->addAction(KStandardAction::Undo, m_doc, SLOT(undo())); a->setWhatsThis(i18n("Revert the most recent editing actions")); a = m_editRedo = ac->addAction(KStandardAction::Redo, m_doc, SLOT(redo())); a->setWhatsThis(i18n("Revert the most recent undo operation")); // Tools > Scripts // stored inside scoped pointer to ensure we destruct it early enough as it does internal cleanups of other child objects m_scriptActionMenu.reset(new KateScriptActionMenu(this, i18n("&Scripts"))); ac->addAction(QStringLiteral("tools_scripts"), m_scriptActionMenu.data()); a = ac->addAction(QStringLiteral("tools_apply_wordwrap")); a->setText(i18n("Apply &Word Wrap")); a->setWhatsThis(i18n("Use this to wrap the current line, or to reformat the selected lines as paragraph, " "to fit the 'Wrap words at' setting in the configuration dialog.

" "This is a static word wrap, meaning the document is changed.")); connect(a, SIGNAL(triggered(bool)), SLOT(applyWordWrap())); a = ac->addAction(QStringLiteral("tools_cleanIndent")); a->setText(i18n("&Clean Indentation")); a->setWhatsThis(i18n("Use this to clean the indentation of a selected block of text (only tabs/only spaces).

" "You can configure whether tabs should be honored and used or replaced with spaces, in the configuration dialog.")); connect(a, SIGNAL(triggered(bool)), SLOT(cleanIndent())); a = ac->addAction(QStringLiteral("tools_align")); a->setText(i18n("&Align")); a->setWhatsThis(i18n("Use this to align the current line or block of text to its proper indent level.")); connect(a, SIGNAL(triggered(bool)), SLOT(align())); a = ac->addAction(QStringLiteral("tools_comment")); a->setText(i18n("C&omment")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_D)); a->setWhatsThis(i18n("This command comments out the current line or a selected block of text.

" "The characters for single/multiple line comments are defined within the language's highlighting.")); connect(a, SIGNAL(triggered(bool)), SLOT(comment())); a = ac->addAction(QStringLiteral("Previous Editing Line")); a->setText(i18n("Go to previous editing line")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(a, SIGNAL(triggered(bool)), SLOT(goToPreviousEditingPosition())); a = ac->addAction(QStringLiteral("Next Editing Line")); a->setText(i18n("Go to next editing line")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_E)); connect(a, SIGNAL(triggered(bool)), SLOT(goToNextEditingPosition())); a = ac->addAction(QStringLiteral("tools_uncomment")); a->setText(i18n("Unco&mment")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_D)); a->setWhatsThis(i18n("This command removes comments from the current line or a selected block of text.

" "The characters for single/multiple line comments are defined within the language's highlighting.")); connect(a, SIGNAL(triggered(bool)), SLOT(uncomment())); a = ac->addAction(QStringLiteral("tools_toggle_comment")); a->setText(i18n("Toggle Comment")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Slash)); connect(a, SIGNAL(triggered(bool)), SLOT(toggleComment())); a = m_toggleWriteLock = new KToggleAction(i18n("&Read Only Mode"), this); a->setWhatsThis(i18n("Lock/unlock the document for writing")); a->setChecked(!doc()->isReadWrite()); connect(a, SIGNAL(triggered(bool)), SLOT(toggleWriteLock())); ac->addAction(QStringLiteral("tools_toggle_write_lock"), a); a = ac->addAction(QStringLiteral("tools_uppercase")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-text-uppercase"))); a->setText(i18n("Uppercase")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_U)); a->setWhatsThis(i18n("Convert the selection to uppercase, or the character to the " "right of the cursor if no text is selected.")); connect(a, SIGNAL(triggered(bool)), SLOT(uppercase())); a = ac->addAction(QStringLiteral("tools_lowercase")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-text-lowercase"))); a->setText(i18n("Lowercase")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_U)); a->setWhatsThis(i18n("Convert the selection to lowercase, or the character to the " "right of the cursor if no text is selected.")); connect(a, SIGNAL(triggered(bool)), SLOT(lowercase())); a = ac->addAction(QStringLiteral("tools_capitalize")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-text-capitalize"))); a->setText(i18n("Capitalize")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_U)); a->setWhatsThis(i18n("Capitalize the selection, or the word under the " "cursor if no text is selected.")); connect(a, SIGNAL(triggered(bool)), SLOT(capitalize())); a = ac->addAction(QStringLiteral("tools_join_lines")); a->setText(i18n("Join Lines")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_J)); connect(a, SIGNAL(triggered(bool)), SLOT(joinLines())); a = ac->addAction(QStringLiteral("tools_invoke_code_completion")); a->setText(i18n("Invoke Code Completion")); a->setWhatsThis(i18n("Manually invoke command completion, usually by using a shortcut bound to this action.")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Space)); connect(a, SIGNAL(triggered(bool)), SLOT(userInvokedCompletion())); } else { m_cut->setEnabled(false); m_paste->setEnabled(false); m_pasteMenu->setEnabled(false); m_editUndo = nullptr; m_editRedo = nullptr; } a = ac->addAction(KStandardAction::Print, this, SLOT(print())); a->setWhatsThis(i18n("Print the current document.")); a = ac->addAction(KStandardAction::PrintPreview, this, SLOT(printPreview())); a->setWhatsThis(i18n("Show print preview of current document")); a = ac->addAction(QStringLiteral("file_reload")); a->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); a->setText(i18n("Reloa&d")); ac->setDefaultShortcuts(a, KStandardShortcut::reload()); a->setWhatsThis(i18n("Reload the current document from disk.")); connect(a, SIGNAL(triggered(bool)), SLOT(reloadFile())); a = ac->addAction(KStandardAction::SaveAs, m_doc, SLOT(documentSaveAs())); a->setWhatsThis(i18n("Save the current document to disk, with a name of your choice.")); a = new KateViewEncodingAction(m_doc, this, i18n("Save As with Encoding..."), this, true /* special mode for save as */); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); ac->addAction(QStringLiteral("file_save_as_with_encoding"), a); a = ac->addAction(QStringLiteral("file_save_copy_as")); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); a->setText(i18n("Save &Copy As...")); a->setWhatsThis(i18n("Save a copy of the current document to disk.")); connect(a, SIGNAL(triggered(bool)), m_doc, SLOT(documentSaveCopyAs())); a = ac->addAction(KStandardAction::GotoLine, this, SLOT(gotoLine())); a->setWhatsThis(i18n("This command opens a dialog and lets you choose a line that you want the cursor to move to.")); a = ac->addAction(QStringLiteral("modified_line_up")); a->setText(i18n("Move to Previous Modified Line")); a->setWhatsThis(i18n("Move upwards to the previous modified line.")); connect(a, SIGNAL(triggered(bool)), SLOT(toPrevModifiedLine())); a = ac->addAction(QStringLiteral("modified_line_down")); a->setText(i18n("Move to Next Modified Line")); a->setWhatsThis(i18n("Move downwards to the next modified line.")); connect(a, SIGNAL(triggered(bool)), SLOT(toNextModifiedLine())); a = ac->addAction(QStringLiteral("set_confdlg")); a->setText(i18n("&Configure Editor...")); a->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); a->setWhatsThis(i18n("Configure various aspects of this editor.")); connect(a, SIGNAL(triggered(bool)), SLOT(slotConfigDialog())); m_modeAction = new KateModeMenu(i18n("&Mode"), this); ac->addAction(QStringLiteral("tools_mode"), m_modeAction); m_modeAction->setWhatsThis(i18n("Here you can choose which mode should be used for the current document. This will influence the highlighting and folding being used, for example.")); m_modeAction->updateMenu(m_doc); KateHighlightingMenu *menu = new KateHighlightingMenu(i18n("&Highlighting"), this); ac->addAction(QStringLiteral("tools_highlighting"), menu); menu->setWhatsThis(i18n("Here you can choose how the current document should be highlighted.")); menu->updateMenu(m_doc); KateViewSchemaAction *schemaMenu = new KateViewSchemaAction(i18n("&Schema"), this); ac->addAction(QStringLiteral("view_schemas"), schemaMenu); schemaMenu->updateMenu(this); // indentation menu KateViewIndentationAction *indentMenu = new KateViewIndentationAction(m_doc, i18n("&Indentation"), this); ac->addAction(QStringLiteral("tools_indentation"), indentMenu); m_selectAll = a = ac->addAction(KStandardAction::SelectAll, this, SLOT(selectAll())); a->setWhatsThis(i18n("Select the entire text of the current document.")); m_deSelect = a = ac->addAction(KStandardAction::Deselect, this, SLOT(clearSelection())); a->setWhatsThis(i18n("If you have selected something within the current document, this will no longer be selected.")); a = ac->addAction(QStringLiteral("view_inc_font_sizes")); a->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); a->setText(i18n("Enlarge Font")); ac->setDefaultShortcuts(a, KStandardShortcut::zoomIn()); a->setWhatsThis(i18n("This increases the display font size.")); connect(a, SIGNAL(triggered(bool)), m_viewInternal, SLOT(slotIncFontSizes())); a = ac->addAction(QStringLiteral("view_dec_font_sizes")); a->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); a->setText(i18n("Shrink Font")); ac->setDefaultShortcuts(a, KStandardShortcut::zoomOut()); a->setWhatsThis(i18n("This decreases the display font size.")); connect(a, SIGNAL(triggered(bool)), m_viewInternal, SLOT(slotDecFontSizes())); a = ac->addAction(QStringLiteral("view_reset_font_sizes")); a->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); a->setText(i18n("Reset Font Size")); ac->setDefaultShortcuts(a, KStandardShortcut::shortcut(KStandardShortcut::ActualSize)); a->setWhatsThis(i18n("This resets the display font size.")); connect(a, SIGNAL(triggered(bool)), m_viewInternal, SLOT(slotResetFontSizes())); a = m_toggleBlockSelection = new KToggleAction(i18n("Bl&ock Selection Mode"), this); ac->addAction(QStringLiteral("set_verticalSelect"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_B)); a->setWhatsThis(i18n("This command allows switching between the normal (line based) selection mode and the block selection mode.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleBlockSelection())); a = ac->addAction(QStringLiteral("switch_next_input_mode")); a->setText(i18n("Switch to Next Input Mode")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_V)); a->setWhatsThis(i18n("Switch to the next input mode.")); connect(a, SIGNAL(triggered(bool)), SLOT(cycleInputMode())); a = m_toggleInsert = new KToggleAction(i18n("Overwr&ite Mode"), this); ac->addAction(QStringLiteral("set_insert"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Insert)); a->setWhatsThis(i18n("Choose whether you want the text you type to be inserted or to overwrite existing text.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleInsert())); KToggleAction *toggleAction; a = m_toggleDynWrap = toggleAction = new KToggleAction(i18n("&Dynamic Word Wrap"), this); ac->addAction(QStringLiteral("view_dynamic_word_wrap"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F10)); a->setWhatsThis(i18n("If this option is checked, the text lines will be wrapped at the view border on the screen.

" "This is only a view option, meaning the document will not changed.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleDynWordWrap())); a = m_setDynWrapIndicators = new KSelectAction(i18n("Dynamic Word Wrap Indicators"), this); ac->addAction(QStringLiteral("dynamic_word_wrap_indicators"), a); a->setWhatsThis(i18n("Choose when the Dynamic Word Wrap Indicators should be displayed")); connect(m_setDynWrapIndicators, SIGNAL(triggered(int)), this, SLOT(setDynWrapIndicators(int))); const QStringList list2{ i18n("&Off"), i18n("Follow &Line Numbers"), i18n("&Always On") }; m_setDynWrapIndicators->setItems(list2); m_setDynWrapIndicators->setEnabled(m_toggleDynWrap->isChecked()); // only synced on real change, later a = toggleAction = new KToggleAction(i18n("Static Word Wrap"), this); ac->addAction(QStringLiteral("view_static_word_wrap"), a); a->setWhatsThis(i18n("If this option is checked, the text lines will be wrapped at the column defined in the editing properties.")); connect(a, &KToggleAction::triggered, [=] { if (m_doc) { m_doc->setWordWrap(!m_doc->wordWrap()); }}); a = toggleAction = m_toggleWWMarker = new KToggleAction(i18n("Show Static &Word Wrap Marker"), this); ac->addAction(QStringLiteral("view_word_wrap_marker"), a); a->setWhatsThis(i18n("Show/hide the Word Wrap Marker, a vertical line drawn at the word " "wrap column as defined in the editing properties")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleWWMarker())); a = toggleAction = m_toggleFoldingMarkers = new KToggleAction(i18n("Show Folding &Markers"), this); ac->addAction(QStringLiteral("view_folding_markers"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F9)); a->setWhatsThis(i18n("You can choose if the codefolding marks should be shown, if codefolding is possible.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleFoldingMarkers())); a = m_toggleIconBar = toggleAction = new KToggleAction(i18n("Show &Icon Border"), this); ac->addAction(QStringLiteral("view_border"), a); a->setWhatsThis(i18n("Show/hide the icon border.

The icon border shows bookmark symbols, for instance.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleIconBorder())); a = toggleAction = m_toggleLineNumbers = new KToggleAction(i18n("Show &Line Numbers"), this); ac->addAction(QStringLiteral("view_line_numbers"), a); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F11)); a->setWhatsThis(i18n("Show/hide the line numbers on the left hand side of the view.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleLineNumbersOn())); a = m_toggleScrollBarMarks = toggleAction = new KToggleAction(i18n("Show Scroll&bar Marks"), this); ac->addAction(QStringLiteral("view_scrollbar_marks"), a); a->setWhatsThis(i18n("Show/hide the marks on the vertical scrollbar.

The marks show bookmarks, for instance.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleScrollBarMarks())); a = m_toggleScrollBarMiniMap = toggleAction = new KToggleAction(i18n("Show Scrollbar Mini-Map"), this); ac->addAction(QStringLiteral("view_scrollbar_minimap"), a); a->setWhatsThis(i18n("Show/hide the mini-map on the vertical scrollbar.

The mini-map shows an overview of the whole document.")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleScrollBarMiniMap())); a = m_doc->autoReloadToggleAction(); ac->addAction(QStringLiteral("view_auto_reload"), a); // a = m_toggleScrollBarMiniMapAll = toggleAction = new KToggleAction(i18n("Show the whole document in the Mini-Map"), this); // ac->addAction(QLatin1String("view_scrollbar_minimap_all"), a); // a->setWhatsThis(i18n("Display the whole document in the mini-map.

With this option set the whole document will be visible in the mini-map.")); // connect(a, SIGNAL(triggered(bool)), SLOT(toggleScrollBarMiniMapAll())); // connect(m_toggleScrollBarMiniMap, SIGNAL(triggered(bool)), m_toggleScrollBarMiniMapAll, SLOT(setEnabled(bool))); a = m_toggleNPSpaces = new KToggleAction(i18n("Show Non-Printable Spaces"), this); ac->addAction(QStringLiteral("view_non_printable_spaces"), a); a->setWhatsThis(i18n("Show/hide bounding box around non-printable spaces")); connect(a, SIGNAL(triggered(bool)), SLOT(toggleNPSpaces())); a = m_switchCmdLine = ac->addAction(QStringLiteral("switch_to_cmd_line")); a->setText(i18n("Switch to Command Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_F7)); a->setWhatsThis(i18n("Show/hide the command line on the bottom of the view.")); connect(a, SIGNAL(triggered(bool)), SLOT(switchToCmdLine())); KActionMenu *am = new KActionMenu(i18n("Input Modes"), this); m_inputModeActions = new QActionGroup(am); ac->addAction(QStringLiteral("view_input_modes"), am); Q_FOREACH(KateAbstractInputMode *mode, m_viewInternal->m_inputModes) { a = new QAction(mode->viewInputModeHuman(), m_inputModeActions); am->addAction(a); a->setWhatsThis(i18n("Activate/deactivate %1", mode->viewInputModeHuman())); const InputMode im = mode->viewInputMode(); a->setData(static_cast(im)); a->setCheckable(true); if (im == m_config->inputMode()) a->setChecked(true); connect(a, SIGNAL(triggered()), SLOT(toggleInputMode())); } a = m_setEndOfLine = new KSelectAction(i18n("&End of Line"), this); ac->addAction(QStringLiteral("set_eol"), a); a->setWhatsThis(i18n("Choose which line endings should be used, when you save the document")); const QStringList list { i18nc("@item:inmenu End of Line", "&UNIX") , i18nc("@item:inmenu End of Line", "&Windows/DOS") , i18nc("@item:inmenu End of Line", "&Macintosh") }; m_setEndOfLine->setItems(list); m_setEndOfLine->setCurrentItem(doc()->config()->eol()); connect(m_setEndOfLine, SIGNAL(triggered(int)), this, SLOT(setEol(int))); a = m_addBom = new KToggleAction(i18n("Add &Byte Order Mark (BOM)"), this); m_addBom->setChecked(doc()->config()->bom()); ac->addAction(QStringLiteral("add_bom"), a); a->setWhatsThis(i18n("Enable/disable adding of byte order marks for UTF-8/UTF-16 encoded files while saving")); connect(m_addBom, SIGNAL(triggered(bool)), this, SLOT(setAddBom(bool))); // encoding menu m_encodingAction = new KateViewEncodingAction(m_doc, this, i18n("E&ncoding"), this); ac->addAction(QStringLiteral("set_encoding"), m_encodingAction); a = ac->addAction(KStandardAction::Find, this, SLOT(find())); a->setWhatsThis(i18n("Look up the first occurrence of a piece of text or regular expression.")); addAction(a); a = ac->addAction(QStringLiteral("edit_find_selected")); a->setText(i18n("Find Selected")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_H)); a->setWhatsThis(i18n("Finds next occurrence of selected text.")); connect(a, SIGNAL(triggered(bool)), SLOT(findSelectedForwards())); a = ac->addAction(QStringLiteral("edit_find_selected_backwards")); a->setText(i18n("Find Selected Backwards")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_H)); a->setWhatsThis(i18n("Finds previous occurrence of selected text.")); connect(a, SIGNAL(triggered(bool)), SLOT(findSelectedBackwards())); a = ac->addAction(KStandardAction::FindNext, this, SLOT(findNext())); a->setWhatsThis(i18n("Look up the next occurrence of the search phrase.")); addAction(a); a = ac->addAction(KStandardAction::FindPrev, QStringLiteral("edit_find_prev"), this, SLOT(findPrevious())); a->setWhatsThis(i18n("Look up the previous occurrence of the search phrase.")); addAction(a); a = ac->addAction(KStandardAction::Replace, this, SLOT(replace())); a->setWhatsThis(i18n("Look up a piece of text or regular expression and replace the result with some given text.")); m_spell->createActions(ac); m_toggleOnTheFlySpellCheck = new KToggleAction(i18n("Automatic Spell Checking"), this); m_toggleOnTheFlySpellCheck->setWhatsThis(i18n("Enable/disable automatic spell checking")); connect(m_toggleOnTheFlySpellCheck, SIGNAL(triggered(bool)), SLOT(toggleOnTheFlySpellCheck(bool))); ac->addAction(QStringLiteral("tools_toggle_automatic_spell_checking"), m_toggleOnTheFlySpellCheck); ac->setDefaultShortcut(m_toggleOnTheFlySpellCheck, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_O)); a = ac->addAction(QStringLiteral("tools_change_dictionary")); a->setText(i18n("Change Dictionary...")); a->setWhatsThis(i18n("Change the dictionary that is used for spell checking.")); connect(a, SIGNAL(triggered()), SLOT(changeDictionary())); a = ac->addAction(QStringLiteral("tools_clear_dictionary_ranges")); a->setText(i18n("Clear Dictionary Ranges")); a->setEnabled(false); a->setWhatsThis(i18n("Remove all the separate dictionary ranges that were set for spell checking.")); connect(a, SIGNAL(triggered()), m_doc, SLOT(clearDictionaryRanges())); connect(m_doc, SIGNAL(dictionaryRangesPresent(bool)), a, SLOT(setEnabled(bool))); m_copyHtmlAction = ac->addAction(QStringLiteral("edit_copy_html"), this, SLOT(exportHtmlToClipboard())); m_copyHtmlAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_copyHtmlAction->setText(i18n("Copy as &HTML")); m_copyHtmlAction->setWhatsThis(i18n("Use this command to copy the currently selected text as HTML to the system clipboard.")); a = ac->addAction(QStringLiteral("file_export_html"), this, SLOT(exportHtmlToFile())); a->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); a->setText(i18n("E&xport as HTML...")); a->setWhatsThis(i18n("This command allows you to export the current document" " with all highlighting information into a HTML document.")); m_spellingMenu->createActions(ac); m_bookmarks->createActions(ac); slotSelectionChanged(); //Now setup the editing actions before adding the associated //widget and setting the shortcut context setupEditActions(); setupCodeFolding(); slotClipboardHistoryChanged(); ac->addAssociatedWidget(m_viewInternal); foreach (QAction *action, ac->actions()) { action->setShortcutContext(Qt::WidgetWithChildrenShortcut); } connect(this, SIGNAL(selectionChanged(KTextEditor::View*)), this, SLOT(slotSelectionChanged())); } void KTextEditor::ViewPrivate::slotConfigDialog() { // invoke config dialog, will auto-save configuration to katepartrc KTextEditor::EditorPrivate::self()->configDialog(this); } void KTextEditor::ViewPrivate::setupEditActions() { //If you add an editing action to this //function make sure to include the line //m_editActions << a after creating the action KActionCollection *ac = actionCollection(); QAction *a = ac->addAction(QStringLiteral("word_left")); a->setText(i18n("Move Word Left")); ac->setDefaultShortcuts(a, KStandardShortcut::backwardWord()); connect(a, SIGNAL(triggered(bool)), SLOT(wordLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("select_char_left")); a->setText(i18n("Select Character Left")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Left)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftCursorLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("select_word_left")); a->setText(i18n("Select Word Left")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_Left)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftWordLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("word_right")); a->setText(i18n("Move Word Right")); ac->setDefaultShortcuts(a, KStandardShortcut::forwardWord()); connect(a, SIGNAL(triggered(bool)), SLOT(wordRight())); m_editActions << a; a = ac->addAction(QStringLiteral("select_char_right")); a->setText(i18n("Select Character Right")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Right)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftCursorRight())); m_editActions << a; a = ac->addAction(QStringLiteral("select_word_right")); a->setText(i18n("Select Word Right")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_Right)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftWordRight())); m_editActions << a; a = ac->addAction(QStringLiteral("beginning_of_line")); a->setText(i18n("Move to Beginning of Line")); ac->setDefaultShortcuts(a, KStandardShortcut::beginningOfLine()); connect(a, SIGNAL(triggered(bool)), SLOT(home())); m_editActions << a; a = ac->addAction(QStringLiteral("beginning_of_document")); a->setText(i18n("Move to Beginning of Document")); ac->setDefaultShortcuts(a, KStandardShortcut::begin()); connect(a, SIGNAL(triggered(bool)), SLOT(top())); m_editActions << a; a = ac->addAction(QStringLiteral("select_beginning_of_line")); a->setText(i18n("Select to Beginning of Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftHome())); m_editActions << a; a = ac->addAction(QStringLiteral("select_beginning_of_document")); a->setText(i18n("Select to Beginning of Document")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftTop())); m_editActions << a; a = ac->addAction(QStringLiteral("end_of_line")); a->setText(i18n("Move to End of Line")); ac->setDefaultShortcuts(a, KStandardShortcut::endOfLine()); connect(a, SIGNAL(triggered(bool)), SLOT(end())); m_editActions << a; a = ac->addAction(QStringLiteral("end_of_document")); a->setText(i18n("Move to End of Document")); ac->setDefaultShortcuts(a, KStandardShortcut::end()); connect(a, SIGNAL(triggered(bool)), SLOT(bottom())); m_editActions << a; a = ac->addAction(QStringLiteral("select_end_of_line")); a->setText(i18n("Select to End of Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftEnd())); m_editActions << a; a = ac->addAction(QStringLiteral("select_end_of_document")); a->setText(i18n("Select to End of Document")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftBottom())); m_editActions << a; a = ac->addAction(QStringLiteral("select_line_up")); a->setText(i18n("Select to Previous Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Up)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftUp())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_line_up")); a->setText(i18n("Scroll Line Up")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Up)); connect(a, SIGNAL(triggered(bool)), SLOT(scrollUp())); m_editActions << a; a = ac->addAction(QStringLiteral("move_line_down")); a->setText(i18n("Move to Next Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Down)); connect(a, SIGNAL(triggered(bool)), SLOT(down())); m_editActions << a; a = ac->addAction(QStringLiteral("move_line_up")); a->setText(i18n("Move to Previous Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Up)); connect(a, SIGNAL(triggered(bool)), SLOT(up())); m_editActions << a; a = ac->addAction(QStringLiteral("move_cursor_right")); a->setText(i18n("Move Cursor Right")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Right)); connect(a, SIGNAL(triggered(bool)), SLOT(cursorRight())); m_editActions << a; a = ac->addAction(QStringLiteral("move_cursor_left")); a->setText(i18n("Move Cursor Left")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Left)); connect(a, SIGNAL(triggered(bool)), SLOT(cursorLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("select_line_down")); a->setText(i18n("Select to Next Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_Down)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftDown())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_line_down")); a->setText(i18n("Scroll Line Down")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_Down)); connect(a, SIGNAL(triggered(bool)), SLOT(scrollDown())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_page_up")); a->setText(i18n("Scroll Page Up")); ac->setDefaultShortcuts(a, KStandardShortcut::prior()); connect(a, SIGNAL(triggered(bool)), SLOT(pageUp())); m_editActions << a; a = ac->addAction(QStringLiteral("select_page_up")); a->setText(i18n("Select Page Up")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_PageUp)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftPageUp())); m_editActions << a; a = ac->addAction(QStringLiteral("move_top_of_view")); a->setText(i18n("Move to Top of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(topOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("select_top_of_view")); a->setText(i18n("Select to Top of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_Home)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftTopOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("scroll_page_down")); a->setText(i18n("Scroll Page Down")); ac->setDefaultShortcuts(a, KStandardShortcut::next()); connect(a, SIGNAL(triggered(bool)), SLOT(pageDown())); m_editActions << a; a = ac->addAction(QStringLiteral("select_page_down")); a->setText(i18n("Select Page Down")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_PageDown)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftPageDown())); m_editActions << a; a = ac->addAction(QStringLiteral("move_bottom_of_view")); a->setText(i18n("Move to Bottom of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(bottomOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("select_bottom_of_view")); a->setText(i18n("Select to Bottom of View")); ac->setDefaultShortcut(a, QKeySequence(Qt::ALT + Qt::SHIFT + Qt::Key_End)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftBottomOfView())); m_editActions << a; a = ac->addAction(QStringLiteral("to_matching_bracket")); a->setText(i18n("Move to Matching Bracket")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_6)); connect(a, SIGNAL(triggered(bool)), SLOT(toMatchingBracket())); //m_editActions << a; a = ac->addAction(QStringLiteral("select_matching_bracket")); a->setText(i18n("Select to Matching Bracket")); ac->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::CTRL + Qt::Key_6)); connect(a, SIGNAL(triggered(bool)), SLOT(shiftToMatchingBracket())); //m_editActions << a; // anders: shortcuts doing any changes should not be created in read-only mode if (!doc()->readOnly()) { a = ac->addAction(QStringLiteral("transpose_char")); a->setText(i18n("Transpose Characters")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(a, SIGNAL(triggered(bool)), SLOT(transpose())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_line")); a->setText(i18n("Delete Line")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_K)); connect(a, SIGNAL(triggered(bool)), SLOT(killLine())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_word_left")); a->setText(i18n("Delete Word Left")); ac->setDefaultShortcuts(a, KStandardShortcut::deleteWordBack()); connect(a, SIGNAL(triggered(bool)), SLOT(deleteWordLeft())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_word_right")); a->setText(i18n("Delete Word Right")); ac->setDefaultShortcuts(a, KStandardShortcut::deleteWordForward()); connect(a, SIGNAL(triggered(bool)), SLOT(deleteWordRight())); m_editActions << a; a = ac->addAction(QStringLiteral("delete_next_character")); a->setText(i18n("Delete Next Character")); ac->setDefaultShortcut(a, QKeySequence(Qt::Key_Delete)); connect(a, SIGNAL(triggered(bool)), SLOT(keyDelete())); m_editActions << a; a = ac->addAction(QStringLiteral("backspace")); a->setText(i18n("Backspace")); QList scuts; scuts << QKeySequence(Qt::Key_Backspace) << QKeySequence(Qt::SHIFT + Qt::Key_Backspace); ac->setDefaultShortcuts(a, scuts); connect(a, SIGNAL(triggered(bool)), SLOT(backspace())); m_editActions << a; a = ac->addAction(QStringLiteral("insert_tabulator")); a->setText(i18n("Insert Tab")); connect(a, SIGNAL(triggered(bool)), SLOT(insertTab())); m_editActions << a; a = ac->addAction(QStringLiteral("smart_newline")); a->setText(i18n("Insert Smart Newline")); a->setWhatsThis(i18n("Insert newline including leading characters of the current line which are not letters or numbers.")); scuts.clear(); scuts << QKeySequence(Qt::SHIFT + Qt::Key_Return) << QKeySequence(Qt::SHIFT + Qt::Key_Enter); ac->setDefaultShortcuts(a, scuts); connect(a, SIGNAL(triggered(bool)), SLOT(smartNewline())); m_editActions << a; a = ac->addAction(QStringLiteral("no_indent_newline")); a->setText(i18n("Insert a non-indented Newline")); a->setWhatsThis(i18n("Insert a new line without indentation, regardless of indentation settings.")); scuts.clear(); scuts << QKeySequence(Qt::CTRL + Qt::Key_Return) << QKeySequence(Qt::CTRL + Qt::Key_Enter); ac->setDefaultShortcuts(a, scuts); connect(a, SIGNAL(triggered(bool)), SLOT(noIndentNewline())); m_editActions << a; a = ac->addAction(QStringLiteral("tools_indent")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-indent-more"))); a->setText(i18n("&Indent")); a->setWhatsThis(i18n("Use this to indent a selected block of text.

" "You can configure whether tabs should be honored and used or replaced with spaces, in the configuration dialog.")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::Key_I)); connect(a, SIGNAL(triggered(bool)), SLOT(indent())); a = ac->addAction(QStringLiteral("tools_unindent")); a->setIcon(QIcon::fromTheme(QStringLiteral("format-indent-less"))); a->setText(i18n("&Unindent")); a->setWhatsThis(i18n("Use this to unindent a selected block of text.")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_I)); connect(a, SIGNAL(triggered(bool)), SLOT(unIndent())); } if (hasFocus()) { slotGotFocus(); } else { slotLostFocus(); } } void KTextEditor::ViewPrivate::setupCodeFolding() { KActionCollection *ac = this->actionCollection(); QAction *a; a = ac->addAction(QStringLiteral("folding_toplevel")); a->setText(i18n("Fold Toplevel Nodes")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Minus)); connect(a, SIGNAL(triggered(bool)), SLOT(slotFoldToplevelNodes())); - a = ac->addAction(QLatin1String("folding_expandtoplevel")); + a = ac->addAction(QStringLiteral("folding_expandtoplevel")); a->setText(i18n("Unfold Toplevel Nodes")); ac->setDefaultShortcut(a, QKeySequence(Qt::CTRL+Qt::SHIFT+Qt::Key_Plus)); connect(a, SIGNAL(triggered(bool)), SLOT(slotExpandToplevelNodes())); /*a = ac->addAction(QLatin1String("folding_expandall")); a->setText(i18n("Unfold All Nodes")); connect(a, SIGNAL(triggered(bool)), doc()->foldingTree(), SLOT(expandAll())); a = ac->addAction(QLatin1String("folding_collapse_dsComment")); a->setText(i18n("Fold Multiline Comments")); connect(a, SIGNAL(triggered(bool)), doc()->foldingTree(), SLOT(collapseAll_dsComments())); */ a = ac->addAction(QStringLiteral("folding_toggle_current")); a->setText(i18n("Toggle Current Node")); connect(a, &QAction::triggered, this, &KTextEditor::ViewPrivate::slotToggleFolding); a = ac->addAction(QStringLiteral("folding_toggle_in_current")); a->setText(i18n("Toggle Contained Nodes")); connect(a, &QAction::triggered, this, &KTextEditor::ViewPrivate::slotToggleFoldingsInRange); } void KTextEditor::ViewPrivate::slotFoldToplevelNodes() { for (int line = 0; line < doc()->lines(); ++line) { if (textFolding().isLineVisible(line)) { foldLine(line); } } } void KTextEditor::ViewPrivate::slotExpandToplevelNodes() { const auto topLevelRanges(textFolding().foldingRangesForParentRange()); for (const auto &range : topLevelRanges) { textFolding().unfoldRange(range.first); } } void KTextEditor::ViewPrivate::slotToggleFolding() { int line = cursorPosition().line(); bool actionDone = false; while (!actionDone && (line > -1)) { actionDone = unfoldLine(line); if (!actionDone) { actionDone = foldLine(line--).isValid(); } } } void KTextEditor::ViewPrivate::slotToggleFoldingsInRange() { int line = cursorPosition().line(); while (!toggleFoldingsInRange(line) && (line > -1)) { --line; } } KTextEditor::Range KTextEditor::ViewPrivate::foldLine(int line) { KTextEditor::Range foldingRange = doc()->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { return foldingRange; } // Ensure not to fold the end marker to avoid a deceptive look, but only on token based folding Kate::TextLine startTextLine = doc()->buffer().plainLine(line); if (!startTextLine->markedAsFoldingStartIndentation()) { const int adjustedLine = foldingRange.end().line() - 1; foldingRange.setEnd(KTextEditor::Cursor(adjustedLine, doc()->buffer().plainLine(adjustedLine)->length())); } // Don't try to fold a single line, which can happens due to adjustment above // FIXME Avoid to offer such a folding marker if (!foldingRange.onSingleLine()) { textFolding().newFoldingRange(foldingRange, Kate::TextFolding::Folded); } return foldingRange; } bool KTextEditor::ViewPrivate::unfoldLine(int line) { bool actionDone = false; const KTextEditor::Cursor currentCursor = cursorPosition(); // ask the folding info for this line, if any folds are around! // auto = QVector> auto startingRanges = textFolding().foldingRangesStartingOnLine(line); for (int i = 0; i < startingRanges.size() && !actionDone; ++i) { // Avoid jumping view in case of a big unfold and ensure nice highlight of folding marker setCursorPosition(textFolding().foldingRange(startingRanges[i].first).start()); actionDone |= textFolding().unfoldRange(startingRanges[i].first); } if (!actionDone) { // Nothing unfolded? Restore old cursor position! setCursorPosition(currentCursor); } return actionDone; } bool KTextEditor::ViewPrivate::toggleFoldingOfLine(int line) { bool actionDone = unfoldLine(line); if (!actionDone) { actionDone = foldLine(line).isValid(); } return actionDone; } bool KTextEditor::ViewPrivate::toggleFoldingsInRange(int line) { KTextEditor::Range foldingRange = doc()->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { // Either line is not valid or there is no start range return false; } bool actionDone = false; // Track success const KTextEditor::Cursor currentCursor = cursorPosition(); // Don't be too eager but obliging! Only toggle containing ranges which are // visible -> Be done when the range is folded actionDone |= unfoldLine(line); if (!actionDone) { // Unfold all in range, but not the range itself for (int ln = foldingRange.start().line() + 1; ln < foldingRange.end().line(); ++ln) { actionDone |= unfoldLine(ln); } if (actionDone) { // In most cases we want now a not moved cursor setCursorPosition(currentCursor); } } if (!actionDone) { // Fold all in range, but not the range itself for (int ln = foldingRange.start().line() + 1; ln < foldingRange.end().line(); ++ln) { KTextEditor::Range fr = foldLine(ln); if (fr.isValid()) { // qMax to avoid infinite loop in case of range without content ln = qMax(ln, fr.end().line() - 1); actionDone = true; } } } if (!actionDone) { // At this point was an unfolded range clicked which contains no "childs" // We assume the user want to fold it by the wrong button, be obliging! actionDone |= foldLine(line).isValid(); } // At this point we should be always true return actionDone; } KTextEditor::View::ViewMode KTextEditor::ViewPrivate::viewMode() const { return currentInputMode()->viewMode(); } QString KTextEditor::ViewPrivate::viewModeHuman() const { QString currentMode = currentInputMode()->viewModeHuman(); /** * append read-only if needed */ if (!doc()->isReadWrite()) { currentMode = i18n("(R/O) %1", currentMode); } /** * return full mode */ return currentMode; } KTextEditor::View::InputMode KTextEditor::ViewPrivate::viewInputMode() const { return currentInputMode()->viewInputMode(); } QString KTextEditor::ViewPrivate::viewInputModeHuman() const { return currentInputMode()->viewInputModeHuman(); } void KTextEditor::ViewPrivate::setInputMode(KTextEditor::View::InputMode mode) { if (currentInputMode()->viewInputMode() == mode) { return; } if (!m_viewInternal->m_inputModes.contains(mode)) { return; } m_viewInternal->m_currentInputMode->deactivate(); m_viewInternal->m_currentInputMode = m_viewInternal->m_inputModes[mode]; m_viewInternal->m_currentInputMode->activate(); config()->setValue(KateViewConfig::InputMode, mode); // TODO: this could be called from read config procedure, so it's not a good idea to set a specific view mode here /* small duplication, but need to do this if not toggled by action */ Q_FOREACH(QAction *action, m_inputModeActions->actions()) { if (static_cast(action->data().toInt()) == mode) { action->setChecked(true); break; } } /* inform the rest of the system about the change */ emit viewInputModeChanged(this, mode); emit viewModeChanged(this, viewMode()); } void KTextEditor::ViewPrivate::slotDocumentAboutToReload() { if (doc()->isAutoReload()) { const int lastVisibleLine = m_viewInternal->endLine(); const int currentLine = cursorPosition().line(); m_gotoBottomAfterReload = (lastVisibleLine == currentLine) && (currentLine == doc()->lastLine()); if (!m_gotoBottomAfterReload) { // Ensure the view jumps not back when user scrolls around const int firstVisibleLine = 1 + lastVisibleLine - m_viewInternal->linesDisplayed(); const int newLine = qBound(firstVisibleLine, currentLine, lastVisibleLine); setCursorPositionVisual(KTextEditor::Cursor(newLine, cursorPosition().column())); } } else { m_gotoBottomAfterReload = false; } } void KTextEditor::ViewPrivate::slotDocumentReloaded() { if (m_gotoBottomAfterReload) { bottom(); } } void KTextEditor::ViewPrivate::slotGotFocus() { //qCDebug(LOG_KTE) << "KTextEditor::ViewPrivate::slotGotFocus"; currentInputMode()->gotFocus(); /** * update current view and scrollbars * it is needed for styles that implement different frame and scrollbar * rendering when focused */ update(); if (m_viewInternal->m_lineScroll->isVisible()) { m_viewInternal->m_lineScroll->update(); } if (m_viewInternal->m_columnScroll->isVisible()) { m_viewInternal->m_columnScroll->update(); } emit focusIn(this); } void KTextEditor::ViewPrivate::slotLostFocus() { //qCDebug(LOG_KTE) << "KTextEditor::ViewPrivate::slotLostFocus"; currentInputMode()->lostFocus(); /** * update current view and scrollbars * it is needed for styles that implement different frame and scrollbar * rendering when focused */ update(); if (m_viewInternal->m_lineScroll->isVisible()) { m_viewInternal->m_lineScroll->update(); } if (m_viewInternal->m_columnScroll->isVisible()) { m_viewInternal->m_columnScroll->update(); } emit focusOut(this); } void KTextEditor::ViewPrivate::setDynWrapIndicators(int mode) { config()->setValue(KateViewConfig::DynWordWrapIndicators, mode); } bool KTextEditor::ViewPrivate::isOverwriteMode() const { return doc()->config()->ovr(); } void KTextEditor::ViewPrivate::reloadFile() { // bookmarks and cursor positions are temporarily saved by the document doc()->documentReload(); } void KTextEditor::ViewPrivate::slotReadWriteChanged() { if (m_toggleWriteLock) { m_toggleWriteLock->setChecked(! doc()->isReadWrite()); } m_cut->setEnabled(doc()->isReadWrite() && (selection() || m_config->smartCopyCut())); m_paste->setEnabled(doc()->isReadWrite()); m_pasteMenu->setEnabled(doc()->isReadWrite() && !KTextEditor::EditorPrivate::self()->clipboardHistory().isEmpty()); m_setEndOfLine->setEnabled(doc()->isReadWrite()); static const auto l = { QStringLiteral("edit_replace") , QStringLiteral("tools_spelling") , QStringLiteral("tools_indent") , QStringLiteral("tools_unindent") , QStringLiteral("tools_cleanIndent") , QStringLiteral("tools_align") , QStringLiteral("tools_comment") , QStringLiteral("tools_uncomment") , QStringLiteral("tools_toggle_comment") , QStringLiteral("tools_uppercase") , QStringLiteral("tools_lowercase") , QStringLiteral("tools_capitalize") , QStringLiteral("tools_join_lines") , QStringLiteral("tools_apply_wordwrap") , QStringLiteral("tools_spelling_from_cursor") , QStringLiteral("tools_spelling_selection") }; foreach (const auto &action, l) { QAction *a = actionCollection()->action(action); if (a) { a->setEnabled(doc()->isReadWrite()); } } slotUpdateUndo(); currentInputMode()->readWriteChanged(doc()->isReadWrite()); // => view mode changed emit viewModeChanged(this, viewMode()); emit viewInputModeChanged(this, viewInputMode()); } void KTextEditor::ViewPrivate::slotClipboardHistoryChanged() { m_pasteMenu->setEnabled(doc()->isReadWrite() && !KTextEditor::EditorPrivate::self()->clipboardHistory().isEmpty()); } void KTextEditor::ViewPrivate::slotUpdateUndo() { if (doc()->readOnly()) { return; } m_editUndo->setEnabled(doc()->isReadWrite() && doc()->undoCount() > 0); m_editRedo->setEnabled(doc()->isReadWrite() && doc()->redoCount() > 0); } bool KTextEditor::ViewPrivate::setCursorPositionInternal(const KTextEditor::Cursor &position, uint tabwidth, bool calledExternally) { Kate::TextLine l = doc()->kateTextLine(position.line()); if (!l) { return false; } QString line_str = doc()->line(position.line()); int x = 0; int z = 0; for (; z < line_str.length() && z < position.column(); z++) { if (line_str[z] == QLatin1Char('\t')) { x += tabwidth - (x % tabwidth); } else { x++; } } if (blockSelection()) if (z < position.column()) { x += position.column() - z; } m_viewInternal->updateCursor(KTextEditor::Cursor(position.line(), x), false, calledExternally /* force center for external calls, see bug 408418 */, calledExternally); return true; } void KTextEditor::ViewPrivate::toggleInsert() { doc()->config()->setOvr(!doc()->config()->ovr()); m_toggleInsert->setChecked(isOverwriteMode()); emit viewModeChanged(this, viewMode()); emit viewInputModeChanged(this, viewInputMode()); } void KTextEditor::ViewPrivate::slotSaveCanceled(const QString &error) { if (!error.isEmpty()) { // happens when canceling a job KMessageBox::error(this, error); } } void KTextEditor::ViewPrivate::gotoLine() { gotoBar()->updateData(); bottomViewBar()->showBarWidget(gotoBar()); } void KTextEditor::ViewPrivate::changeDictionary() { dictionaryBar()->updateData(); bottomViewBar()->showBarWidget(dictionaryBar()); } void KTextEditor::ViewPrivate::joinLines() { int first = selectionRange().start().line(); int last = selectionRange().end().line(); //int left = doc()->line( last ).length() - doc()->selEndCol(); if (first == last) { first = cursorPosition().line(); last = first + 1; } doc()->joinLines(first, last); } void KTextEditor::ViewPrivate::readSessionConfig(const KConfigGroup &config, const QSet &flags) { Q_UNUSED(flags) // cursor position setCursorPositionInternal(KTextEditor::Cursor(config.readEntry("CursorLine", 0), config.readEntry("CursorColumn", 0))); m_config->setDynWordWrap(config.readEntry("Dynamic Word Wrap", false)); // restore text folding m_savedFoldingState = QJsonDocument::fromJson(config.readEntry("TextFolding", QByteArray())); applyFoldingState(); Q_FOREACH(KateAbstractInputMode *mode, m_viewInternal->m_inputModes) { mode->readSessionConfig(config); } } void KTextEditor::ViewPrivate::writeSessionConfig(KConfigGroup &config, const QSet &flags) { Q_UNUSED(flags) // cursor position config.writeEntry("CursorLine", cursorPosition().line()); config.writeEntry("CursorColumn", cursorPosition().column()); config.writeEntry("Dynamic Word Wrap", m_config->dynWordWrap()); // save text folding state saveFoldingState(); config.writeEntry("TextFolding", m_savedFoldingState.toJson(QJsonDocument::Compact)); m_savedFoldingState = QJsonDocument(); Q_FOREACH(KateAbstractInputMode *mode, m_viewInternal->m_inputModes) { mode->writeSessionConfig(config); } } int KTextEditor::ViewPrivate::getEol() const { return doc()->config()->eol(); } void KTextEditor::ViewPrivate::setEol(int eol) { if (!doc()->isReadWrite()) { return; } if (m_updatingDocumentConfig) { return; } if (eol != doc()->config()->eol()) { doc()->setModified(true); // mark modified (bug #143120) doc()->config()->setEol(eol); } } void KTextEditor::ViewPrivate::setAddBom(bool enabled) { if (!doc()->isReadWrite()) { return; } if (m_updatingDocumentConfig) { return; } doc()->config()->setBom(enabled); doc()->bomSetByUser(); } void KTextEditor::ViewPrivate::setIconBorder(bool enable) { config()->setValue(KateViewConfig::ShowIconBar, enable); } void KTextEditor::ViewPrivate::toggleIconBorder() { config()->setValue(KateViewConfig::ShowIconBar, !config()->iconBar()); } void KTextEditor::ViewPrivate::setLineNumbersOn(bool enable) { config()->setValue(KateViewConfig::ShowLineNumbers, enable); } void KTextEditor::ViewPrivate::toggleLineNumbersOn() { config()->setValue(KateViewConfig::ShowLineNumbers, !config()->lineNumbers()); } void KTextEditor::ViewPrivate::setScrollBarMarks(bool enable) { config()->setValue(KateViewConfig::ShowScrollBarMarks, enable); } void KTextEditor::ViewPrivate::toggleScrollBarMarks() { config()->setValue(KateViewConfig::ShowScrollBarMarks, !config()->scrollBarMarks()); } void KTextEditor::ViewPrivate::setScrollBarMiniMap(bool enable) { config()->setValue(KateViewConfig::ShowScrollBarMiniMap, enable); } void KTextEditor::ViewPrivate::toggleScrollBarMiniMap() { config()->setValue(KateViewConfig::ShowScrollBarMiniMap, !config()->scrollBarMiniMap()); } void KTextEditor::ViewPrivate::setScrollBarMiniMapAll(bool enable) { config()->setValue(KateViewConfig::ShowScrollBarMiniMapAll, enable); } void KTextEditor::ViewPrivate::toggleScrollBarMiniMapAll() { config()->setValue(KateViewConfig::ShowScrollBarMiniMapAll, !config()->scrollBarMiniMapAll()); } void KTextEditor::ViewPrivate::setScrollBarMiniMapWidth(int width) { config()->setValue(KateViewConfig::ScrollBarMiniMapWidth, width); } void KTextEditor::ViewPrivate::toggleDynWordWrap() { config()->setDynWordWrap(!config()->dynWordWrap()); } void KTextEditor::ViewPrivate::toggleWWMarker() { m_renderer->config()->setWordWrapMarker(!m_renderer->config()->wordWrapMarker()); } void KTextEditor::ViewPrivate::toggleNPSpaces() { m_renderer->setShowNonPrintableSpaces(!m_renderer->showNonPrintableSpaces()); m_viewInternal->update(); // force redraw } void KTextEditor::ViewPrivate::toggleWordCount(bool on) { config()->setShowWordCount(on); } void KTextEditor::ViewPrivate::setFoldingMarkersOn(bool enable) { config()->setValue(KateViewConfig::ShowFoldingBar, enable); } void KTextEditor::ViewPrivate::toggleFoldingMarkers() { config()->setValue(KateViewConfig::ShowFoldingBar, !config()->foldingBar()); } bool KTextEditor::ViewPrivate::iconBorder() { return m_viewInternal->m_leftBorder->iconBorderOn(); } bool KTextEditor::ViewPrivate::lineNumbersOn() { return m_viewInternal->m_leftBorder->lineNumbersOn(); } bool KTextEditor::ViewPrivate::scrollBarMarks() { return m_viewInternal->m_lineScroll->showMarks(); } bool KTextEditor::ViewPrivate::scrollBarMiniMap() { return m_viewInternal->m_lineScroll->showMiniMap(); } int KTextEditor::ViewPrivate::dynWrapIndicators() { return m_viewInternal->m_leftBorder->dynWrapIndicators(); } bool KTextEditor::ViewPrivate::foldingMarkersOn() { return m_viewInternal->m_leftBorder->foldingMarkersOn(); } void KTextEditor::ViewPrivate::toggleWriteLock() { doc()->setReadWrite(! doc()->isReadWrite()); } void KTextEditor::ViewPrivate::registerTextHintProvider(KTextEditor::TextHintProvider *provider) { m_viewInternal->registerTextHintProvider(provider); } void KTextEditor::ViewPrivate::unregisterTextHintProvider(KTextEditor::TextHintProvider *provider) { m_viewInternal->unregisterTextHintProvider(provider); } void KTextEditor::ViewPrivate::setTextHintDelay(int delay) { m_viewInternal->setTextHintDelay(delay); } int KTextEditor::ViewPrivate::textHintDelay() const { return m_viewInternal->textHintDelay(); } void KTextEditor::ViewPrivate::find() { currentInputMode()->find(); } void KTextEditor::ViewPrivate::findSelectedForwards() { currentInputMode()->findSelectedForwards(); } void KTextEditor::ViewPrivate::findSelectedBackwards() { currentInputMode()->findSelectedBackwards(); } void KTextEditor::ViewPrivate::replace() { currentInputMode()->findReplace(); } void KTextEditor::ViewPrivate::findNext() { currentInputMode()->findNext(); } void KTextEditor::ViewPrivate::findPrevious() { currentInputMode()->findPrevious(); } void KTextEditor::ViewPrivate::slotSelectionChanged() { m_copy->setEnabled(selection() || m_config->smartCopyCut()); m_deSelect->setEnabled(selection()); m_copyHtmlAction->setEnabled (selection()); // update highlighting of current selected word selectionChangedForHighlights (); if (doc()->readOnly()) { return; } m_cut->setEnabled(selection() || m_config->smartCopyCut()); } void KTextEditor::ViewPrivate::switchToCmdLine() { currentInputMode()->activateCommandLine(); } KateRenderer *KTextEditor::ViewPrivate::renderer() { return m_renderer; } void KTextEditor::ViewPrivate::updateConfig() { if (m_startingUp) { return; } // dyn. word wrap & markers if (m_hasWrap != config()->dynWordWrap()) { m_viewInternal->prepareForDynWrapChange(); m_hasWrap = config()->dynWordWrap(); m_viewInternal->dynWrapChanged(); m_setDynWrapIndicators->setEnabled(config()->dynWordWrap()); m_toggleDynWrap->setChecked(config()->dynWordWrap()); } m_viewInternal->m_leftBorder->setDynWrapIndicators(config()->dynWordWrapIndicators()); m_setDynWrapIndicators->setCurrentItem(config()->dynWordWrapIndicators()); // line numbers m_viewInternal->m_leftBorder->setLineNumbersOn(config()->lineNumbers()); m_toggleLineNumbers->setChecked(config()->lineNumbers()); // icon bar m_viewInternal->m_leftBorder->setIconBorderOn(config()->iconBar()); m_toggleIconBar->setChecked(config()->iconBar()); // scrollbar marks m_viewInternal->m_lineScroll->setShowMarks(config()->scrollBarMarks()); m_toggleScrollBarMarks->setChecked(config()->scrollBarMarks()); // scrollbar mini-map m_viewInternal->m_lineScroll->setShowMiniMap(config()->scrollBarMiniMap()); m_toggleScrollBarMiniMap->setChecked(config()->scrollBarMiniMap()); // scrollbar mini-map - (whole document) m_viewInternal->m_lineScroll->setMiniMapAll(config()->scrollBarMiniMapAll()); //m_toggleScrollBarMiniMapAll->setChecked( config()->scrollBarMiniMapAll() ); // scrollbar mini-map.width m_viewInternal->m_lineScroll->setMiniMapWidth(config()->scrollBarMiniMapWidth()); // misc edit m_toggleBlockSelection->setChecked(blockSelection()); m_toggleInsert->setChecked(isOverwriteMode()); updateFoldingConfig(); // bookmark m_bookmarks->setSorting((KateBookmarks::Sorting) config()->bookmarkSort()); m_viewInternal->setAutoCenterLines(config()->autoCenterLines()); Q_FOREACH(KateAbstractInputMode *input, m_viewInternal->m_inputModes) { input->updateConfig(); } setInputMode(config()->inputMode()); reflectOnTheFlySpellCheckStatus(doc()->isOnTheFlySpellCheckingEnabled()); // register/unregister word completion... bool wc = config()->wordCompletion(); if (wc != isCompletionModelRegistered(KTextEditor::EditorPrivate::self()->wordCompletionModel())) { if (wc) registerCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); else unregisterCompletionModel(KTextEditor::EditorPrivate::self()->wordCompletionModel()); } bool kc = config()->keywordCompletion(); if (kc != isCompletionModelRegistered(KTextEditor::EditorPrivate::self()->keywordCompletionModel())) { if (kc) registerCompletionModel(KTextEditor::EditorPrivate::self()->keywordCompletionModel()); else unregisterCompletionModel (KTextEditor::EditorPrivate::self()->keywordCompletionModel()); } m_cut->setEnabled(doc()->isReadWrite() && (selection() || m_config->smartCopyCut())); m_copy->setEnabled(selection() || m_config->smartCopyCut()); // if not disabled, update status bar if (m_statusBar) { m_statusBar->updateStatus(); } // now redraw... m_viewInternal->cache()->clear(); tagAll(); updateView(true); emit configChanged(); } void KTextEditor::ViewPrivate::updateDocumentConfig() { if (m_startingUp) { return; } m_updatingDocumentConfig = true; m_setEndOfLine->setCurrentItem(doc()->config()->eol()); m_addBom->setChecked(doc()->config()->bom()); m_updatingDocumentConfig = false; // maybe block selection or wrap-cursor mode changed ensureCursorColumnValid(); // first change this m_renderer->setTabWidth(doc()->config()->tabWidth()); m_renderer->setIndentWidth(doc()->config()->indentationWidth()); // now redraw... m_viewInternal->cache()->clear(); tagAll(); updateView(true); } void KTextEditor::ViewPrivate::updateRendererConfig() { if (m_startingUp) { return; } m_toggleWWMarker->setChecked(m_renderer->config()->wordWrapMarker()); m_viewInternal->updateBracketMarkAttributes(); m_viewInternal->updateBracketMarks(); // now redraw... m_viewInternal->cache()->clear(); tagAll(); m_viewInternal->updateView(true); // update the left border right, for example linenumbers m_viewInternal->m_leftBorder->updateFont(); m_viewInternal->m_leftBorder->repaint(); m_viewInternal->m_lineScroll->queuePixmapUpdate(); currentInputMode()->updateRendererConfig(); // @@ showIndentLines is not cached anymore. // m_renderer->setShowIndentLines (m_renderer->config()->showIndentationLines()); emit configChanged(); } void KTextEditor::ViewPrivate::updateFoldingConfig() { // folding bar m_viewInternal->m_leftBorder->setFoldingMarkersOn(config()->foldingBar()); m_toggleFoldingMarkers->setChecked(config()->foldingBar()); if (hasCommentInFirstLine(m_doc)) { if (config()->foldFirstLine() && !m_autoFoldedFirstLine) { foldLine(0); m_autoFoldedFirstLine = true; } else if (!config()->foldFirstLine() && m_autoFoldedFirstLine) { unfoldLine(0); m_autoFoldedFirstLine = false; } } else { m_autoFoldedFirstLine = false; } #if 0 // FIXME: FOLDING const QStringList l = { QStringLiteral("folding_toplevel") , QStringLiteral("folding_expandtoplevel") , QStringLiteral("folding_toggle_current") , QStringLiteral("folding_toggle_in_current") }; QAction *a = 0; for (int z = 0; z < l.size(); z++) if ((a = actionCollection()->action(l[z].toAscii().constData()))) { a->setEnabled(doc()->highlight() && doc()->highlight()->allowsFolding()); } #endif } void KTextEditor::ViewPrivate::ensureCursorColumnValid() { KTextEditor::Cursor c = m_viewInternal->cursorPosition(); // make sure the cursor is valid: // - in block selection mode or if wrap cursor is off, the column is arbitrary // - otherwise: it's bounded by the line length if (!blockSelection() && wrapCursor() && (!c.isValid() || c.column() > doc()->lineLength(c.line()))) { c.setColumn(doc()->kateTextLine(cursorPosition().line())->length()); setCursorPosition(c); } } //BEGIN EDIT STUFF void KTextEditor::ViewPrivate::editStart() { m_viewInternal->editStart(); } void KTextEditor::ViewPrivate::editEnd(int editTagLineStart, int editTagLineEnd, bool tagFrom) { m_viewInternal->editEnd(editTagLineStart, editTagLineEnd, tagFrom); } void KTextEditor::ViewPrivate::editSetCursor(const KTextEditor::Cursor &cursor) { m_viewInternal->editSetCursor(cursor); } //END //BEGIN TAG & CLEAR bool KTextEditor::ViewPrivate::tagLine(const KTextEditor::Cursor &virtualCursor) { return m_viewInternal->tagLine(virtualCursor); } bool KTextEditor::ViewPrivate::tagRange(const KTextEditor::Range &range, bool realLines) { return m_viewInternal->tagRange(range, realLines); } bool KTextEditor::ViewPrivate::tagLines(int start, int end, bool realLines) { return m_viewInternal->tagLines(start, end, realLines); } bool KTextEditor::ViewPrivate::tagLines(KTextEditor::Cursor start, KTextEditor::Cursor end, bool realCursors) { return m_viewInternal->tagLines(start, end, realCursors); } void KTextEditor::ViewPrivate::tagAll() { m_viewInternal->tagAll(); } void KTextEditor::ViewPrivate::clear() { m_viewInternal->clear(); } void KTextEditor::ViewPrivate::repaintText(bool paintOnlyDirty) { if (paintOnlyDirty) { m_viewInternal->updateDirty(); } else { m_viewInternal->update(); } } void KTextEditor::ViewPrivate::updateView(bool changed) { //qCDebug(LOG_KTE) << "KTextEditor::ViewPrivate::updateView"; m_viewInternal->updateView(changed); m_viewInternal->m_leftBorder->update(); } //END void KTextEditor::ViewPrivate::slotHlChanged() { KateHighlighting *hl = doc()->highlight(); bool ok(!hl->getCommentStart(0).isEmpty() || !hl->getCommentSingleLineStart(0).isEmpty()); if (actionCollection()->action(QStringLiteral("tools_comment"))) { actionCollection()->action(QStringLiteral("tools_comment"))->setEnabled(ok); } if (actionCollection()->action(QStringLiteral("tools_uncomment"))) { actionCollection()->action(QStringLiteral("tools_uncomment"))->setEnabled(ok); } if (actionCollection()->action(QStringLiteral("tools_toggle_comment"))) { actionCollection()->action(QStringLiteral("tools_toggle_comment"))->setEnabled(ok); } // show folding bar if "view defaults" says so, otherwise enable/disable only the menu entry updateFoldingConfig(); } int KTextEditor::ViewPrivate::virtualCursorColumn() const { return doc()->toVirtualColumn(m_viewInternal->cursorPosition()); } void KTextEditor::ViewPrivate::notifyMousePositionChanged(const KTextEditor::Cursor &newPosition) { emit mousePositionChanged(this, newPosition); } //BEGIN KTextEditor::SelectionInterface stuff bool KTextEditor::ViewPrivate::setSelection(const KTextEditor::Range &selection) { /** * anything to do? */ if (selection == m_selection) { return true; } /** * backup old range */ KTextEditor::Range oldSelection = m_selection; /** * set new range */ m_selection.setRange(selection.isEmpty() ? KTextEditor::Range::invalid() : selection); /** * trigger update of correct area */ tagSelection(oldSelection); repaintText(true); /** * emit holy signal */ emit selectionChanged(this); /** * be done */ return true; } bool KTextEditor::ViewPrivate::clearSelection() { return clearSelection(true); } bool KTextEditor::ViewPrivate::clearSelection(bool redraw, bool finishedChangingSelection) { /** * no selection, nothing to do... */ if (!selection()) { return false; } /** * backup old range */ KTextEditor::Range oldSelection = m_selection; /** * invalidate current selection */ m_selection.setRange(KTextEditor::Range::invalid()); /** * trigger update of correct area */ tagSelection(oldSelection); if (redraw) { repaintText(true); } /** * emit holy signal */ if (finishedChangingSelection) { emit selectionChanged(this); } /** * be done */ return true; } bool KTextEditor::ViewPrivate::selection() const { if (!wrapCursor()) { return m_selection != KTextEditor::Range::invalid(); } else { return m_selection.toRange().isValid(); } } QString KTextEditor::ViewPrivate::selectionText() const { return doc()->text(m_selection, blockSelect); } bool KTextEditor::ViewPrivate::removeSelectedText() { if (!selection()) { return false; } doc()->editStart(); // Optimization: clear selection before removing text KTextEditor::Range selection = m_selection; doc()->removeText(selection, blockSelect); // don't redraw the cleared selection - that's done in editEnd(). if (blockSelect) { int selectionColumn = qMin(doc()->toVirtualColumn(selection.start()), doc()->toVirtualColumn(selection.end())); KTextEditor::Range newSelection = selection; newSelection.setStart(KTextEditor::Cursor(newSelection.start().line(), doc()->fromVirtualColumn(newSelection.start().line(), selectionColumn))); newSelection.setEnd(KTextEditor::Cursor(newSelection.end().line(), doc()->fromVirtualColumn(newSelection.end().line(), selectionColumn))); setSelection(newSelection); setCursorPositionInternal(newSelection.start()); } else { clearSelection(false); } doc()->editEnd(); return true; } bool KTextEditor::ViewPrivate::selectAll() { setBlockSelection(false); top(); shiftBottom(); return true; } bool KTextEditor::ViewPrivate::cursorSelected(const KTextEditor::Cursor &cursor) { KTextEditor::Cursor ret = cursor; if ((!blockSelect) && (ret.column() < 0)) { ret.setColumn(0); } if (blockSelect) return cursor.line() >= m_selection.start().line() && ret.line() <= m_selection.end().line() && ret.column() >= m_selection.start().column() && ret.column() <= m_selection.end().column(); else { return m_selection.toRange().contains(cursor) || m_selection.end() == cursor; } } bool KTextEditor::ViewPrivate::lineSelected(int line) { return !blockSelect && m_selection.toRange().containsLine(line); } bool KTextEditor::ViewPrivate::lineEndSelected(const KTextEditor::Cursor &lineEndPos) { return (!blockSelect) && (lineEndPos.line() > m_selection.start().line() || (lineEndPos.line() == m_selection.start().line() && (m_selection.start().column() < lineEndPos.column() || lineEndPos.column() == -1))) && (lineEndPos.line() < m_selection.end().line() || (lineEndPos.line() == m_selection.end().line() && (lineEndPos.column() <= m_selection.end().column() && lineEndPos.column() != -1))); } bool KTextEditor::ViewPrivate::lineHasSelected(int line) { return selection() && m_selection.toRange().containsLine(line); } bool KTextEditor::ViewPrivate::lineIsSelection(int line) { return (line == m_selection.start().line() && line == m_selection.end().line()); } void KTextEditor::ViewPrivate::tagSelection(const KTextEditor::Range &oldSelection) { if (selection()) { if (oldSelection.start().line() == -1) { // We have to tag the whole lot if // 1) we have a selection, and: // a) it's new; or tagLines(m_selection, true); } else if (blockSelection() && (oldSelection.start().column() != m_selection.start().column() || oldSelection.end().column() != m_selection.end().column())) { // b) we're in block selection mode and the columns have changed tagLines(m_selection, true); tagLines(oldSelection, true); } else { if (oldSelection.start() != m_selection.start()) { if (oldSelection.start() < m_selection.start()) { tagLines(oldSelection.start(), m_selection.start(), true); } else { tagLines(m_selection.start(), oldSelection.start(), true); } } if (oldSelection.end() != m_selection.end()) { if (oldSelection.end() < m_selection.end()) { tagLines(oldSelection.end(), m_selection.end(), true); } else { tagLines(m_selection.end(), oldSelection.end(), true); } } } } else { // No more selection, clean up tagLines(oldSelection, true); } } void KTextEditor::ViewPrivate::selectWord(const KTextEditor::Cursor &cursor) { setSelection(doc()->wordRangeAt(cursor)); } void KTextEditor::ViewPrivate::selectLine(const KTextEditor::Cursor &cursor) { int line = cursor.line(); if (line + 1 >= doc()->lines()) { setSelection(KTextEditor::Range(line, 0, line, doc()->lineLength(line))); } else { setSelection(KTextEditor::Range(line, 0, line + 1, 0)); } } void KTextEditor::ViewPrivate::cut() { if (!selection() && !m_config->smartCopyCut()) { return; } copy(); if (!selection()) { selectLine(cursorPosition()); } removeSelectedText(); } void KTextEditor::ViewPrivate::copy() const { QString text; if (!selection()) { if (!m_config->smartCopyCut()) { return; } text = doc()->line(cursorPosition().line()) + QLatin1Char('\n'); m_viewInternal->moveEdge(KateViewInternal::left, false); } else { text = selectionText(); } // copy to clipboard and our history! KTextEditor::EditorPrivate::self()->copyToClipboard(text); } void KTextEditor::ViewPrivate::applyWordWrap() { int first = selectionRange().start().line(); int last = selectionRange().end().line(); if (first == last) { // Either no selection or only one line selected, wrap only the current line first = cursorPosition().line(); last = first; } doc()->wrapParagraph(first, last); } //END //BEGIN KTextEditor::BlockSelectionInterface stuff bool KTextEditor::ViewPrivate::blockSelection() const { return blockSelect; } bool KTextEditor::ViewPrivate::setBlockSelection(bool on) { if (on != blockSelect) { blockSelect = on; KTextEditor::Range oldSelection = m_selection; const bool hadSelection = clearSelection(false, false); setSelection(oldSelection); m_toggleBlockSelection->setChecked(blockSelection()); // when leaving block selection mode, if cursor is at an invalid position or past the end of the // line, move the cursor to the last column of the current line unless cursor wrapping is off ensureCursorColumnValid(); if (!hadSelection) { // emit selectionChanged() according to the KTextEditor::View api // documentation also if there is no selection around. This is needed, // as e.g. the Kate App status bar uses this signal to update the state // of the selection mode (block selection, line based selection) emit selectionChanged(this); } } return true; } bool KTextEditor::ViewPrivate::toggleBlockSelection() { m_toggleBlockSelection->setChecked(!blockSelect); return setBlockSelection(!blockSelect); } bool KTextEditor::ViewPrivate::wrapCursor() const { return !blockSelection(); } //END void KTextEditor::ViewPrivate::slotTextInserted(KTextEditor::View *view, const KTextEditor::Cursor &position, const QString &text) { emit textInserted(view, position, text); } bool KTextEditor::ViewPrivate::insertTemplateInternal(const KTextEditor::Cursor& c, const QString& templateString, const QString& script) { /** * no empty templates */ if (templateString.isEmpty()) { return false; } /** * not for read-only docs */ if (!doc()->isReadWrite()) { return false; } /** * only one handler maybe active at a time; store it in the document. * Clear it first to make sure at no time two handlers are active at once */ doc()->setActiveTemplateHandler(nullptr); doc()->setActiveTemplateHandler(new KateTemplateHandler(this, c, templateString, script, doc()->undoManager())); return true; } bool KTextEditor::ViewPrivate::tagLines(KTextEditor::Range range, bool realRange) { return tagLines(range.start(), range.end(), realRange); } void KTextEditor::ViewPrivate::deactivateEditActions() { foreach (QAction *action, m_editActions) { action->setEnabled(false); } } void KTextEditor::ViewPrivate::activateEditActions() { foreach (QAction *action, m_editActions) { action->setEnabled(true); } } bool KTextEditor::ViewPrivate::mouseTrackingEnabled() const { // FIXME support return true; } bool KTextEditor::ViewPrivate::setMouseTrackingEnabled(bool) { // FIXME support return true; } bool KTextEditor::ViewPrivate::isCompletionActive() const { return completionWidget()->isCompletionActive(); } KateCompletionWidget *KTextEditor::ViewPrivate::completionWidget() const { if (!m_completionWidget) { m_completionWidget = new KateCompletionWidget(const_cast(this)); } return m_completionWidget; } void KTextEditor::ViewPrivate::startCompletion(const KTextEditor::Range &word, KTextEditor::CodeCompletionModel *model) { completionWidget()->startCompletion(word, model); } void KTextEditor::ViewPrivate::abortCompletion() { completionWidget()->abortCompletion(); } void KTextEditor::ViewPrivate::forceCompletion() { completionWidget()->execute(); } void KTextEditor::ViewPrivate::registerCompletionModel(KTextEditor::CodeCompletionModel *model) { completionWidget()->registerCompletionModel(model); } void KTextEditor::ViewPrivate::unregisterCompletionModel(KTextEditor::CodeCompletionModel *model) { completionWidget()->unregisterCompletionModel(model); } bool KTextEditor::ViewPrivate::isCompletionModelRegistered(KTextEditor::CodeCompletionModel *model) const { return completionWidget()->isCompletionModelRegistered(model); } bool KTextEditor::ViewPrivate::isAutomaticInvocationEnabled() const { return !m_temporaryAutomaticInvocationDisabled && m_config->automaticCompletionInvocation(); } void KTextEditor::ViewPrivate::setAutomaticInvocationEnabled(bool enabled) { config()->setValue(KateViewConfig::AutomaticCompletionInvocation, enabled); } void KTextEditor::ViewPrivate::sendCompletionExecuted(const KTextEditor::Cursor &position, KTextEditor::CodeCompletionModel *model, const QModelIndex &index) { emit completionExecuted(this, position, model, index); } void KTextEditor::ViewPrivate::sendCompletionAborted() { emit completionAborted(this); } void KTextEditor::ViewPrivate::paste(const QString *textToPaste) { m_temporaryAutomaticInvocationDisabled = true; doc()->paste(this, textToPaste ? *textToPaste : QApplication::clipboard()->text(QClipboard::Clipboard)); m_temporaryAutomaticInvocationDisabled = false; } bool KTextEditor::ViewPrivate::setCursorPosition(KTextEditor::Cursor position) { return setCursorPositionInternal(position, 1, true); } KTextEditor::Cursor KTextEditor::ViewPrivate::cursorPosition() const { return m_viewInternal->cursorPosition(); } KTextEditor::Cursor KTextEditor::ViewPrivate::cursorPositionVirtual() const { return KTextEditor::Cursor(m_viewInternal->cursorPosition().line(), virtualCursorColumn()); } QPoint KTextEditor::ViewPrivate::cursorToCoordinate(const KTextEditor::Cursor &cursor) const { // map from ViewInternal to View coordinates const QPoint pt = m_viewInternal->cursorToCoordinate(cursor, true, false); return pt == QPoint(-1, -1) ? pt : m_viewInternal->mapToParent(pt); } KTextEditor::Cursor KTextEditor::ViewPrivate::coordinatesToCursor(const QPoint &coords) const { // map from View to ViewInternal coordinates return m_viewInternal->coordinatesToCursor(m_viewInternal->mapFromParent(coords), false); } QPoint KTextEditor::ViewPrivate::cursorPositionCoordinates() const { // map from ViewInternal to View coordinates const QPoint pt = m_viewInternal->cursorCoordinates(false); return pt == QPoint(-1, -1) ? pt : m_viewInternal->mapToParent(pt); } void KTextEditor::ViewPrivate::setScrollPositionInternal(KTextEditor::Cursor &cursor) { m_viewInternal->scrollPos(cursor, false, true, false); } void KTextEditor::ViewPrivate::setHorizontalScrollPositionInternal(int x) { m_viewInternal->scrollColumns(x); } KTextEditor::Cursor KTextEditor::ViewPrivate::maxScrollPositionInternal() const { return m_viewInternal->maxStartPos(true); } int KTextEditor::ViewPrivate::firstDisplayedLineInternal(LineType lineType) const { if (lineType == RealLine) { return m_textFolding.visibleLineToLine(m_viewInternal->startLine()); } else { return m_viewInternal->startLine(); } } int KTextEditor::ViewPrivate::lastDisplayedLineInternal(LineType lineType) const { if (lineType == RealLine) { return m_textFolding.visibleLineToLine(m_viewInternal->endLine()); } else { return m_viewInternal->endLine(); } } QRect KTextEditor::ViewPrivate::textAreaRectInternal() const { const auto sourceRect = m_viewInternal->rect(); const auto topLeft = m_viewInternal->mapTo(this, sourceRect.topLeft()); const auto bottomRight = m_viewInternal->mapTo(this, sourceRect.bottomRight()); return {topLeft, bottomRight}; } bool KTextEditor::ViewPrivate::setCursorPositionVisual(const KTextEditor::Cursor &position) { return setCursorPositionInternal(position, doc()->config()->tabWidth(), true); } QString KTextEditor::ViewPrivate::currentTextLine() { return doc()->line(cursorPosition().line()); } QTextLayout * KTextEditor::ViewPrivate::textLayout(int line) const { KateLineLayoutPtr thisLine = m_viewInternal->cache()->line(line); return thisLine->isValid() ? thisLine->layout() : nullptr; } QTextLayout * KTextEditor::ViewPrivate::textLayout(const KTextEditor::Cursor &pos) const { KateLineLayoutPtr thisLine = m_viewInternal->cache()->line(pos); return thisLine->isValid() ? thisLine->layout() : nullptr; } void KTextEditor::ViewPrivate::indent() { KTextEditor::Cursor c(cursorPosition().line(), 0); KTextEditor::Range r = selection() ? selectionRange() : KTextEditor::Range(c, c); doc()->indent(r, 1); } void KTextEditor::ViewPrivate::unIndent() { KTextEditor::Cursor c(cursorPosition().line(), 0); KTextEditor::Range r = selection() ? selectionRange() : KTextEditor::Range(c, c); doc()->indent(r, -1); } void KTextEditor::ViewPrivate::cleanIndent() { KTextEditor::Cursor c(cursorPosition().line(), 0); KTextEditor::Range r = selection() ? selectionRange() : KTextEditor::Range(c, c); doc()->indent(r, 0); } void KTextEditor::ViewPrivate::align() { // no selection: align current line; selection: use selection range const int line = cursorPosition().line(); KTextEditor::Range alignRange(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, 0)); if (selection()) { alignRange = selectionRange(); } doc()->align(this, alignRange); } void KTextEditor::ViewPrivate::comment() { m_selection.setInsertBehaviors(Kate::TextRange::ExpandLeft | Kate::TextRange::ExpandRight); doc()->comment(this, cursorPosition().line(), cursorPosition().column(), 1); m_selection.setInsertBehaviors(Kate::TextRange::ExpandRight); } void KTextEditor::ViewPrivate::uncomment() { doc()->comment(this, cursorPosition().line(), cursorPosition().column(), -1); } void KTextEditor::ViewPrivate::toggleComment() { m_selection.setInsertBehaviors(Kate::TextRange::ExpandLeft | Kate::TextRange::ExpandRight); doc()->comment(this, cursorPosition().line(), cursorPosition().column(), 0); m_selection.setInsertBehaviors(Kate::TextRange::ExpandRight); } void KTextEditor::ViewPrivate::uppercase() { doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Uppercase); } void KTextEditor::ViewPrivate::killLine() { if (m_selection.isEmpty()) { doc()->removeLine(cursorPosition().line()); } else { doc()->editStart(); // cache endline, else that moves and we might delete complete document if last line is selected! for (int line = m_selection.end().line(), endLine = m_selection.start().line(); line >= endLine; line--) { doc()->removeLine(line); } doc()->editEnd(); } } void KTextEditor::ViewPrivate::lowercase() { doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Lowercase); } void KTextEditor::ViewPrivate::capitalize() { doc()->editStart(); doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Lowercase); doc()->transform(this, cursorPosition(), KTextEditor::DocumentPrivate::Capitalize); doc()->editEnd(); } void KTextEditor::ViewPrivate::keyReturn() { doc()->newLine(this); m_viewInternal->iconBorder()->updateForCursorLineChange(); m_viewInternal->updateView(); } void KTextEditor::ViewPrivate::smartNewline() { const KTextEditor::Cursor cursor = cursorPosition(); const int ln = cursor.line(); Kate::TextLine line = doc()->kateTextLine(ln); int col = qMin(cursor.column(), line->firstChar()); if (col != -1) { while (line->length() > col && !(line->at(col).isLetterOrNumber() || line->at(col) == QLatin1Char('_')) && col < cursor.column()) { ++col; } } else { col = line->length(); // stay indented } doc()->editStart(); doc()->editWrapLine(ln, cursor.column()); doc()->insertText(KTextEditor::Cursor(ln + 1, 0), line->string(0, col)); doc()->editEnd(); m_viewInternal->updateView(); } void KTextEditor::ViewPrivate::noIndentNewline() { doc()->newLine(this, KTextEditor::DocumentPrivate::NoIndent); m_viewInternal->iconBorder()->updateForCursorLineChange(); m_viewInternal->updateView(); } void KTextEditor::ViewPrivate::backspace() { doc()->backspace(this, cursorPosition()); } void KTextEditor::ViewPrivate::insertTab() { doc()->insertTab(this, cursorPosition()); } void KTextEditor::ViewPrivate::deleteWordLeft() { doc()->editStart(); m_viewInternal->wordPrev(true); KTextEditor::Range selection = selectionRange(); removeSelectedText(); doc()->editEnd(); m_viewInternal->tagRange(selection, true); m_viewInternal->updateDirty(); } void KTextEditor::ViewPrivate::keyDelete() { doc()->del(this, cursorPosition()); } void KTextEditor::ViewPrivate::deleteWordRight() { doc()->editStart(); m_viewInternal->wordNext(true); KTextEditor::Range selection = selectionRange(); removeSelectedText(); doc()->editEnd(); m_viewInternal->tagRange(selection, true); m_viewInternal->updateDirty(); } void KTextEditor::ViewPrivate::transpose() { doc()->transpose(cursorPosition()); } void KTextEditor::ViewPrivate::cursorLeft() { if (selection() && !config()->persistentSelection()) { if (currentTextLine().isRightToLeft()) { m_viewInternal->updateCursor(selectionRange().end()); setSelection(KTextEditor::Range::invalid()); } else { m_viewInternal->updateCursor(selectionRange().start()); setSelection(KTextEditor::Range::invalid()); } } else { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorNextChar(); } else { m_viewInternal->cursorPrevChar(); } } } void KTextEditor::ViewPrivate::shiftCursorLeft() { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorNextChar(true); } else { m_viewInternal->cursorPrevChar(true); } } void KTextEditor::ViewPrivate::cursorRight() { if (selection() && !config()->persistentSelection()) { if (currentTextLine().isRightToLeft()) { m_viewInternal->updateCursor(selectionRange().start()); setSelection(KTextEditor::Range::invalid()); } else { m_viewInternal->updateCursor(selectionRange().end()); setSelection(KTextEditor::Range::invalid()); } } else { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorPrevChar(); } else { m_viewInternal->cursorNextChar(); } } } void KTextEditor::ViewPrivate::shiftCursorRight() { if (currentTextLine().isRightToLeft()) { m_viewInternal->cursorPrevChar(true); } else { m_viewInternal->cursorNextChar(true); } } void KTextEditor::ViewPrivate::wordLeft() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordNext(); } else { m_viewInternal->wordPrev(); } } void KTextEditor::ViewPrivate::shiftWordLeft() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordNext(true); } else { m_viewInternal->wordPrev(true); } } void KTextEditor::ViewPrivate::wordRight() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordPrev(); } else { m_viewInternal->wordNext(); } } void KTextEditor::ViewPrivate::shiftWordRight() { if (currentTextLine().isRightToLeft()) { m_viewInternal->wordPrev(true); } else { m_viewInternal->wordNext(true); } } void KTextEditor::ViewPrivate::home() { m_viewInternal->home(); } void KTextEditor::ViewPrivate::shiftHome() { m_viewInternal->home(true); } void KTextEditor::ViewPrivate::end() { m_viewInternal->end(); } void KTextEditor::ViewPrivate::shiftEnd() { m_viewInternal->end(true); } void KTextEditor::ViewPrivate::up() { m_viewInternal->cursorUp(); } void KTextEditor::ViewPrivate::shiftUp() { m_viewInternal->cursorUp(true); } void KTextEditor::ViewPrivate::down() { m_viewInternal->cursorDown(); } void KTextEditor::ViewPrivate::shiftDown() { m_viewInternal->cursorDown(true); } void KTextEditor::ViewPrivate::scrollUp() { m_viewInternal->scrollUp(); } void KTextEditor::ViewPrivate::scrollDown() { m_viewInternal->scrollDown(); } void KTextEditor::ViewPrivate::topOfView() { m_viewInternal->topOfView(); } void KTextEditor::ViewPrivate::shiftTopOfView() { m_viewInternal->topOfView(true); } void KTextEditor::ViewPrivate::bottomOfView() { m_viewInternal->bottomOfView(); } void KTextEditor::ViewPrivate::shiftBottomOfView() { m_viewInternal->bottomOfView(true); } void KTextEditor::ViewPrivate::pageUp() { m_viewInternal->pageUp(); } void KTextEditor::ViewPrivate::shiftPageUp() { m_viewInternal->pageUp(true); } void KTextEditor::ViewPrivate::pageDown() { m_viewInternal->pageDown(); } void KTextEditor::ViewPrivate::shiftPageDown() { m_viewInternal->pageDown(true); } void KTextEditor::ViewPrivate::top() { m_viewInternal->top_home(); } void KTextEditor::ViewPrivate::shiftTop() { m_viewInternal->top_home(true); } void KTextEditor::ViewPrivate::bottom() { m_viewInternal->bottom_end(); } void KTextEditor::ViewPrivate::shiftBottom() { m_viewInternal->bottom_end(true); } void KTextEditor::ViewPrivate::toMatchingBracket() { m_viewInternal->cursorToMatchingBracket(); } void KTextEditor::ViewPrivate::shiftToMatchingBracket() { m_viewInternal->cursorToMatchingBracket(true); } void KTextEditor::ViewPrivate::toPrevModifiedLine() { const int startLine = cursorPosition().line() - 1; const int line = doc()->findTouchedLine(startLine, false); if (line >= 0) { KTextEditor::Cursor c(line, 0); m_viewInternal->updateSelection(c, false); m_viewInternal->updateCursor(c); } } void KTextEditor::ViewPrivate::toNextModifiedLine() { const int startLine = cursorPosition().line() + 1; const int line = doc()->findTouchedLine(startLine, true); if (line >= 0) { KTextEditor::Cursor c(line, 0); m_viewInternal->updateSelection(c, false); m_viewInternal->updateCursor(c); } } KTextEditor::Range KTextEditor::ViewPrivate::selectionRange() const { return m_selection; } KTextEditor::Document *KTextEditor::ViewPrivate::document() const { return m_doc; } void KTextEditor::ViewPrivate::setContextMenu(QMenu *menu) { if (m_contextMenu) { disconnect(m_contextMenu, SIGNAL(aboutToShow()), this, SLOT(aboutToShowContextMenu())); disconnect(m_contextMenu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideContextMenu())); } m_contextMenu = menu; m_userContextMenuSet = true; if (m_contextMenu) { connect(m_contextMenu, SIGNAL(aboutToShow()), this, SLOT(aboutToShowContextMenu())); connect(m_contextMenu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideContextMenu())); } } QMenu *KTextEditor::ViewPrivate::contextMenu() const { if (m_userContextMenuSet) { return m_contextMenu; } else { KXMLGUIClient *client = const_cast(this); while (client->parentClient()) { client = client->parentClient(); } //qCDebug(LOG_KTE) << "looking up all menu containers"; if (client->factory()) { QList conts = client->factory()->containers(QStringLiteral("menu")); foreach (QWidget *w, conts) { if (w->objectName() == QLatin1String("ktexteditor_popup")) { //perhaps optimize this block QMenu *menu = (QMenu *)w; // menu is a reusable instance shared among all views. Therefore, // disconnect the current receiver(s) from the menu show/hide signals // before connecting `this` view. This ensures that only the current // view gets a signal when the menu is about to be shown or hidden, // and not also the view(s) that previously had the menu open. disconnect(menu, SIGNAL(aboutToShow()), nullptr, nullptr); disconnect(menu, SIGNAL(aboutToHide()), nullptr, nullptr); connect(menu, SIGNAL(aboutToShow()), this, SLOT(aboutToShowContextMenu())); connect(menu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideContextMenu())); return menu; } } } } return nullptr; } QMenu *KTextEditor::ViewPrivate::defaultContextMenu(QMenu *menu) const { if (!menu) { menu = new QMenu(const_cast(this)); } menu->addAction(m_editUndo); menu->addAction(m_editRedo); menu->addSeparator(); menu->addAction(m_cut); menu->addAction(m_copy); menu->addAction(m_paste); menu->addSeparator(); menu->addAction(m_selectAll); menu->addAction(m_deSelect); if (QAction *spellingSuggestions = actionCollection()->action(QStringLiteral("spelling_suggestions"))) { menu->addSeparator(); menu->addAction(spellingSuggestions); } if (QAction *bookmark = actionCollection()->action(QStringLiteral("bookmarks"))) { menu->addSeparator(); menu->addAction(bookmark); } return menu; } void KTextEditor::ViewPrivate::aboutToShowContextMenu() { QMenu *menu = qobject_cast(sender()); if (menu) { emit contextMenuAboutToShow(this, menu); } } void KTextEditor::ViewPrivate::aboutToHideContextMenu() { m_spellingMenu->setUseMouseForMisspelledRange(false); } // BEGIN ConfigInterface stff QStringList KTextEditor::ViewPrivate::configKeys() const { static const QStringList keys = { QStringLiteral("icon-bar"), QStringLiteral("line-numbers"), QStringLiteral("dynamic-word-wrap"), QStringLiteral("background-color"), QStringLiteral("selection-color"), QStringLiteral("search-highlight-color"), QStringLiteral("replace-highlight-color"), QStringLiteral("default-mark-type"), QStringLiteral("allow-mark-menu"), QStringLiteral("folding-bar"), QStringLiteral("folding-preview"), QStringLiteral("icon-border-color"), QStringLiteral("folding-marker-color"), QStringLiteral("line-number-color"), QStringLiteral("current-line-number-color"), QStringLiteral("modification-markers"), QStringLiteral("keyword-completion"), QStringLiteral("word-count"), QStringLiteral("scrollbar-minimap"), QStringLiteral("scrollbar-preview"), QStringLiteral("font") }; return keys; } QVariant KTextEditor::ViewPrivate::configValue(const QString &key) { if (key == QLatin1String("icon-bar")) { return config()->iconBar(); } else if (key == QLatin1String("line-numbers")) { return config()->lineNumbers(); } else if (key == QLatin1String("dynamic-word-wrap")) { return config()->dynWordWrap(); } else if (key == QLatin1String("background-color")) { return renderer()->config()->backgroundColor(); } else if (key == QLatin1String("selection-color")) { return renderer()->config()->selectionColor(); } else if (key == QLatin1String("search-highlight-color")) { return renderer()->config()->searchHighlightColor(); } else if (key == QLatin1String("replace-highlight-color")) { return renderer()->config()->replaceHighlightColor(); } else if (key == QLatin1String("default-mark-type")) { return config()->defaultMarkType(); } else if (key == QLatin1String("allow-mark-menu")) { return config()->allowMarkMenu(); } else if (key == QLatin1String("folding-bar")) { return config()->foldingBar(); } else if (key == QLatin1String("folding-preview")) { return config()->foldingPreview(); } else if (key == QLatin1String("icon-border-color")) { return renderer()->config()->iconBarColor(); } else if (key == QLatin1String("folding-marker-color")) { return renderer()->config()->foldingColor(); } else if (key == QLatin1String("line-number-color")) { return renderer()->config()->lineNumberColor(); } else if (key == QLatin1String("current-line-number-color")) { return renderer()->config()->currentLineNumberColor(); } else if (key == QLatin1String("modification-markers")) { return config()->lineModification(); } else if (key == QLatin1String("keyword-completion")) { return config()->keywordCompletion(); } else if (key == QLatin1String("word-count")) { return config()->showWordCount(); } else if (key == QLatin1String("scrollbar-minimap")) { return config()->scrollBarMiniMap(); } else if (key == QLatin1String("scrollbar-preview")) { return config()->scrollBarPreview(); } else if (key == QLatin1String("font")) { return renderer()->config()->font(); } // return invalid variant return QVariant(); } void KTextEditor::ViewPrivate::setConfigValue(const QString &key, const QVariant &value) { // First, try the new config interface if (config()->setValue(key, value)) { return; } else if (renderer()->config()->setValue(key, value)) { return; } // No success? Go the old way if (value.canConvert(QVariant::Color)) { if (key == QLatin1String("background-color")) { renderer()->config()->setBackgroundColor(value.value()); } else if (key == QLatin1String("selection-color")) { renderer()->config()->setSelectionColor(value.value()); } else if (key == QLatin1String("search-highlight-color")) { renderer()->config()->setSearchHighlightColor(value.value()); } else if (key == QLatin1String("replace-highlight-color")) { renderer()->config()->setReplaceHighlightColor(value.value()); } else if (key == QLatin1String("icon-border-color")) { renderer()->config()->setIconBarColor(value.value()); } else if (key == QLatin1String("folding-marker-color")) { renderer()->config()->setFoldingColor(value.value()); } else if (key == QLatin1String("line-number-color")) { renderer()->config()->setLineNumberColor(value.value()); } else if (key == QLatin1String("current-line-number-color")) { renderer()->config()->setCurrentLineNumberColor(value.value()); } } else if (value.type() == QVariant::Bool) { // Note explicit type check above. If we used canConvert, then // values of type UInt will be trapped here. if (key == QLatin1String("dynamic-word-wrap")) { config()->setDynWordWrap(value.toBool()); } else if (key == QLatin1String("word-count")) { config()->setShowWordCount(value.toBool()); } } else if (value.canConvert(QVariant::Font)) { if (key == QLatin1String("font")) { renderer()->config()->setFont(value.value()); } } } // END ConfigInterface void KTextEditor::ViewPrivate::userInvokedCompletion() { completionWidget()->userInvokedCompletion(); } KateViewBar *KTextEditor::ViewPrivate::bottomViewBar() const { return m_bottomViewBar; } KateGotoBar *KTextEditor::ViewPrivate::gotoBar() { if (!m_gotoBar) { m_gotoBar = new KateGotoBar(this); bottomViewBar()->addBarWidget(m_gotoBar); } return m_gotoBar; } KateDictionaryBar *KTextEditor::ViewPrivate::dictionaryBar() { if (!m_dictionaryBar) { m_dictionaryBar = new KateDictionaryBar(this); bottomViewBar()->addBarWidget(m_dictionaryBar); } return m_dictionaryBar; } void KTextEditor::ViewPrivate::setAnnotationModel(KTextEditor::AnnotationModel *model) { KTextEditor::AnnotationModel *oldmodel = m_annotationModel; m_annotationModel = model; m_viewInternal->m_leftBorder->annotationModelChanged(oldmodel, m_annotationModel); } KTextEditor::AnnotationModel *KTextEditor::ViewPrivate::annotationModel() const { return m_annotationModel; } void KTextEditor::ViewPrivate::setAnnotationBorderVisible(bool visible) { m_viewInternal->m_leftBorder->setAnnotationBorderOn(visible); } bool KTextEditor::ViewPrivate::isAnnotationBorderVisible() const { return m_viewInternal->m_leftBorder->annotationBorderOn(); } KTextEditor::AbstractAnnotationItemDelegate* KTextEditor::ViewPrivate::annotationItemDelegate() const { return m_viewInternal->m_leftBorder->annotationItemDelegate(); } void KTextEditor::ViewPrivate::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate) { m_viewInternal->m_leftBorder->setAnnotationItemDelegate(delegate); } bool KTextEditor::ViewPrivate::uniformAnnotationItemSizes() const { return m_viewInternal->m_leftBorder->uniformAnnotationItemSizes(); } void KTextEditor::ViewPrivate::setAnnotationUniformItemSizes(bool enable) { m_viewInternal->m_leftBorder->setAnnotationUniformItemSizes(enable); } KTextEditor::Range KTextEditor::ViewPrivate::visibleRange() { //ensure that the view is up-to-date, otherwise 'endPos()' might fail! if (!m_viewInternal->endPos().isValid()) { m_viewInternal->updateView(); } return KTextEditor::Range(m_viewInternal->toRealCursor(m_viewInternal->startPos()), m_viewInternal->toRealCursor(m_viewInternal->endPos())); } bool KTextEditor::ViewPrivate::event(QEvent *e) { switch (e->type()) { case QEvent::StyleChange: setupLayout(); return true; default: return KTextEditor::View::event(e); } } void KTextEditor::ViewPrivate::paintEvent(QPaintEvent *e) { //base class KTextEditor::View::paintEvent(e); const QRect contentsRect = m_topSpacer->geometry()| m_bottomSpacer->geometry()| m_leftSpacer->geometry()| m_rightSpacer->geometry(); if (contentsRect.isValid()) { QStyleOptionFrame opt; opt.initFrom(this); opt.frameShape = QFrame::StyledPanel; opt.state |= QStyle::State_Sunken; // clear mouseOver and focus state // update from relevant widgets opt.state &= ~(QStyle::State_HasFocus|QStyle::State_MouseOver); const QList widgets = QList() << m_viewInternal << m_viewInternal->m_leftBorder << m_viewInternal->m_lineScroll << m_viewInternal->m_columnScroll; foreach (const QWidget *w, widgets) { if (w->hasFocus()) opt.state |= QStyle::State_HasFocus; if (w->underMouse()) opt.state |= QStyle::State_MouseOver; } // update rect opt.rect=contentsRect; // render QPainter paint(this); paint.setClipRegion(e->region()); paint.setRenderHints(QPainter::Antialiasing); style()->drawControl(QStyle::CE_ShapedFrame, &opt, &paint, this); } } void KTextEditor::ViewPrivate::toggleOnTheFlySpellCheck(bool b) { doc()->onTheFlySpellCheckingEnabled(b); } void KTextEditor::ViewPrivate::reflectOnTheFlySpellCheckStatus(bool enabled) { m_spellingMenu->setVisible(enabled); m_toggleOnTheFlySpellCheck->setChecked(enabled); } KateSpellingMenu *KTextEditor::ViewPrivate::spellingMenu() { return m_spellingMenu; } void KTextEditor::ViewPrivate::notifyAboutRangeChange(int startLine, int endLine, bool rangeWithAttribute) { #ifdef VIEW_RANGE_DEBUG // output args qCDebug(LOG_KTE) << "trigger attribute changed from" << startLine << "to" << endLine << "rangeWithAttribute" << rangeWithAttribute; #endif // first call: if (!m_delayedUpdateTriggered) { m_delayedUpdateTriggered = true; m_lineToUpdateMin = -1; m_lineToUpdateMax = -1; // only set initial line range, if range with attribute! if (rangeWithAttribute) { m_lineToUpdateMin = startLine; m_lineToUpdateMax = endLine; } // emit queued signal and be done emit delayedUpdateOfView(); return; } // ignore lines if no attribute if (!rangeWithAttribute) { return; } // update line range if (startLine != -1 && (m_lineToUpdateMin == -1 || startLine < m_lineToUpdateMin)) { m_lineToUpdateMin = startLine; } if (endLine != -1 && endLine > m_lineToUpdateMax) { m_lineToUpdateMax = endLine; } } void KTextEditor::ViewPrivate::slotDelayedUpdateOfView() { if (!m_delayedUpdateTriggered) { return; } #ifdef VIEW_RANGE_DEBUG // output args qCDebug(LOG_KTE) << "delayed attribute changed from" << m_lineToUpdateMin << "to" << m_lineToUpdateMax; #endif // update ranges in updateRangesIn(KTextEditor::Attribute::ActivateMouseIn); updateRangesIn(KTextEditor::Attribute::ActivateCaretIn); // update view, if valid line range, else only feedback update wanted anyway if (m_lineToUpdateMin != -1 && m_lineToUpdateMax != -1) { tagLines(m_lineToUpdateMin, m_lineToUpdateMax, true); updateView(true); } // reset flags m_delayedUpdateTriggered = false; m_lineToUpdateMin = -1; m_lineToUpdateMax = -1; } void KTextEditor::ViewPrivate::updateRangesIn(KTextEditor::Attribute::ActivationType activationType) { // new ranges with cursor in, default none QSet newRangesIn; // on which range set we work? QSet &oldSet = (activationType == KTextEditor::Attribute::ActivateMouseIn) ? m_rangesMouseIn : m_rangesCaretIn; // which cursor position to honor? KTextEditor::Cursor currentCursor = (activationType == KTextEditor::Attribute::ActivateMouseIn) ? m_viewInternal->mousePosition() : m_viewInternal->cursorPosition(); // first: validate the remembered ranges QSet validRanges; foreach (Kate::TextRange *range, oldSet) if (doc()->buffer().rangePointerValid(range)) { validRanges.insert(range); } // cursor valid? else no new ranges can be found if (currentCursor.isValid() && currentCursor.line() < doc()->buffer().lines()) { // now: get current ranges for the line of cursor with an attribute QList rangesForCurrentCursor = doc()->buffer().rangesForLine(currentCursor.line(), this, false); // match which ranges really fit the given cursor foreach (Kate::TextRange *range, rangesForCurrentCursor) { // range has no dynamic attribute of right type and no feedback object if ((!range->attribute() || !range->attribute()->dynamicAttribute(activationType)) && !range->feedback()) { continue; } // range doesn't contain cursor, not interesting if ((range->start().insertBehavior() == KTextEditor::MovingCursor::StayOnInsert) ? (currentCursor < range->start().toCursor()) : (currentCursor <= range->start().toCursor())) { continue; } if ((range->end().insertBehavior() == KTextEditor::MovingCursor::StayOnInsert) ? (range->end().toCursor() <= currentCursor) : (range->end().toCursor() < currentCursor)) { continue; } // range contains cursor, was it already in old set? if (validRanges.contains(range)) { // insert in new, remove from old, be done with it newRangesIn.insert(range); validRanges.remove(range); continue; } // oh, new range, trigger update and insert into new set newRangesIn.insert(range); if (range->attribute() && range->attribute()->dynamicAttribute(activationType)) { notifyAboutRangeChange(range->start().line(), range->end().line(), true); } // feedback if (range->feedback()) { if (activationType == KTextEditor::Attribute::ActivateMouseIn) { range->feedback()->mouseEnteredRange(range, this); } else { range->feedback()->caretEnteredRange(range, this); emit caretChangedRange(this); } } #ifdef VIEW_RANGE_DEBUG // found new range for activation qCDebug(LOG_KTE) << "activated new range" << range << "by" << activationType; #endif } } // now: notify for left ranges! foreach (Kate::TextRange *range, validRanges) { // range valid + right dynamic attribute, trigger update if (range->toRange().isValid() && range->attribute() && range->attribute()->dynamicAttribute(activationType)) { notifyAboutRangeChange(range->start().line(), range->end().line(), true); } // feedback if (range->feedback()) { if (activationType == KTextEditor::Attribute::ActivateMouseIn) { range->feedback()->mouseExitedRange(range, this); } else { range->feedback()->caretExitedRange(range, this); emit caretChangedRange(this); } } } // set new ranges oldSet = newRangesIn; } void KTextEditor::ViewPrivate::postMessage(KTextEditor::Message *message, QList > actions) { // just forward to KateMessageWidget :-) auto messageWidget = m_messageWidgets[message->position()]; if (!messageWidget) { // this branch is used for: TopInView, CenterInView, and BottomInView messageWidget = new KateMessageWidget(m_viewInternal, true); m_messageWidgets[message->position()] = messageWidget; m_notificationLayout->addWidget(messageWidget, message->position()); connect(this, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate*)), messageWidget, SLOT(startAutoHideTimer())); connect(this, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)), messageWidget, SLOT(startAutoHideTimer())); } messageWidget->postMessage(message, actions); } KateMessageWidget *KTextEditor::ViewPrivate::messageWidget() { return m_messageWidgets[KTextEditor::Message::TopInView]; } void KTextEditor::ViewPrivate::saveFoldingState() { m_savedFoldingState = m_textFolding.exportFoldingRanges(); } void KTextEditor::ViewPrivate::applyFoldingState() { m_textFolding.importFoldingRanges(m_savedFoldingState); m_savedFoldingState = QJsonDocument(); } void KTextEditor::ViewPrivate::exportHtmlToFile(const QString &file) { KateExporter(this).exportToFile(file); } void KTextEditor::ViewPrivate::exportHtmlToClipboard () { KateExporter(this).exportToClipboard(); } void KTextEditor::ViewPrivate::exportHtmlToFile () { const QString file = QFileDialog::getSaveFileName(this, i18n("Export File as HTML"), doc()->documentName()); if (!file.isEmpty()) { KateExporter(this).exportToFile(file); } } void KTextEditor::ViewPrivate::clearHighlights() { qDeleteAll(m_rangesForHighlights); m_rangesForHighlights.clear(); m_currentTextForHighlights.clear(); } void KTextEditor::ViewPrivate::selectionChangedForHighlights() { QString text; // if text of selection is still the same, abort if (selection() && selectionRange().onSingleLine()) { text = selectionText(); if (text == m_currentTextForHighlights) return; } // text changed: remove all highlights + create new ones // (do not call clearHighlights(), since this also resets the m_currentTextForHighlights qDeleteAll(m_rangesForHighlights); m_rangesForHighlights.clear(); // do not highlight strings with leading and trailing spaces if (!text.isEmpty() && (text.at(0).isSpace() || text.at(text.length()-1).isSpace())) return; // trigger creation of ranges for current view range m_currentTextForHighlights = text; createHighlights(); } void KTextEditor::ViewPrivate::createHighlights() { // do nothing if no text to highlight if (m_currentTextForHighlights.isEmpty()) { return; } KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); attr->setBackground(Qt::yellow); // set correct highlight color from Kate's color schema QColor fgColor = defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); QColor bgColor = renderer()->config()->searchHighlightColor(); attr->setForeground(fgColor); attr->setBackground(bgColor); KTextEditor::Cursor start(visibleRange().start()); KTextEditor::Range searchRange; /** * only add word boundary if we can find the text then * fixes $lala hl */ QString regex = QRegExp::escape (m_currentTextForHighlights); if (QRegExp (QStringLiteral("\\b%1").arg(regex)).indexIn (QStringLiteral(" %1 ").arg(m_currentTextForHighlights)) != -1) regex = QStringLiteral("\\b%1").arg(regex); if (QRegExp (QStringLiteral("%1\\b").arg(regex)).indexIn (QStringLiteral(" %1 ").arg(m_currentTextForHighlights)) != -1) regex = QStringLiteral("%1\\b").arg(regex); QVector matches; do { searchRange.setRange(start, visibleRange().end()); matches = doc()->searchText(searchRange, regex, KTextEditor::Regex); if (matches.first().isValid()) { KTextEditor::MovingRange* mr = doc()->newMovingRange(matches.first()); mr->setAttribute(attr); mr->setView(this); mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection mr->setAttributeOnlyForViews(true); m_rangesForHighlights.append(mr); start = matches.first().end(); } } while (matches.first().isValid()); } KateAbstractInputMode *KTextEditor::ViewPrivate::currentInputMode() const { return m_viewInternal->m_currentInputMode; } void KTextEditor::ViewPrivate::toggleInputMode() { if (QAction *a = dynamic_cast(sender())) { setInputMode(static_cast(a->data().toInt())); } } void KTextEditor::ViewPrivate::cycleInputMode() { InputMode current = currentInputMode()->viewInputMode(); InputMode to = (current == KTextEditor::View::NormalInputMode) ? KTextEditor::View::ViInputMode : KTextEditor::View::NormalInputMode; setInputMode(to); } //BEGIN KTextEditor::PrintInterface stuff bool KTextEditor::ViewPrivate::print() { return KatePrinter::print(this); } void KTextEditor::ViewPrivate::printPreview() { KatePrinter::printPreview(this); } //END //BEGIN KTextEditor::InlineNoteInterface void KTextEditor::ViewPrivate::registerInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) { if (! m_inlineNoteProviders.contains(provider)) { m_inlineNoteProviders.append(provider); connect(provider, &KTextEditor::InlineNoteProvider::inlineNotesReset, this, &ViewPrivate::inlineNotesReset); connect(provider, &KTextEditor::InlineNoteProvider::inlineNotesChanged, this, &ViewPrivate::inlineNotesLineChanged); inlineNotesReset(); } } void KTextEditor::ViewPrivate::unregisterInlineNoteProvider(KTextEditor::InlineNoteProvider *provider) { const int index = m_inlineNoteProviders.indexOf(provider); if (index >= 0) { m_inlineNoteProviders.removeAt(index); provider->disconnect(this); inlineNotesReset(); } } QVarLengthArray KTextEditor::ViewPrivate::inlineNotes(int line) const { QVarLengthArray allInlineNotes; for (KTextEditor::InlineNoteProvider *provider: m_inlineNoteProviders) { int index = 0; for (auto column: provider->inlineNotes(line)) { KateInlineNoteData note = { provider, this, {line, column}, index, m_viewInternal->m_activeInlineNote.m_underMouse, m_viewInternal->renderer()->currentFont(), m_viewInternal->renderer()->lineHeight() }; allInlineNotes.append(note); index++; } } return allInlineNotes; } QRect KTextEditor::ViewPrivate::inlineNoteRect(const KateInlineNoteData& note) const { return m_viewInternal->inlineNoteRect(note); } void KTextEditor::ViewPrivate::inlineNotesReset() { m_viewInternal->m_activeInlineNote = {}; tagLines(0, doc()->lastLine(), true); } void KTextEditor::ViewPrivate::inlineNotesLineChanged(int line) { if ( line == m_viewInternal->m_activeInlineNote.m_position.line() ) { m_viewInternal->m_activeInlineNote = {}; } tagLines(line, line, true); } //END KTextEditor::InlineNoteInterface KTextEditor::Attribute::Ptr KTextEditor::ViewPrivate::defaultStyleAttribute(KTextEditor::DefaultStyle defaultStyle) const { KateRendererConfig * renderConfig = const_cast(this)->renderer()->config(); KTextEditor::Attribute::Ptr style = doc()->highlight()->attributes(renderConfig->schema()).at(defaultStyle); if (!style->hasProperty(QTextFormat::BackgroundBrush)) { // make sure the returned style has the default background color set style = new KTextEditor::Attribute(*style); style->setBackground(QBrush(renderConfig->backgroundColor())); } return style; } QList KTextEditor::ViewPrivate::lineAttributes(int line) { QList attribs; if (line < 0 || line >= doc()->lines()) return attribs; Kate::TextLine kateLine = doc()->kateTextLine(line); if (!kateLine) { return attribs; } const QVector &intAttrs = kateLine->attributesList(); for (int i = 0; i < intAttrs.size(); ++i) { if (intAttrs[i].length > 0 && intAttrs[i].attributeValue > 0) { attribs << KTextEditor::AttributeBlock( intAttrs.at(i).offset, intAttrs.at(i).length, renderer()->attribute(intAttrs.at(i).attributeValue) ); } } return attribs; } diff --git a/src/view/kateviewhelpers.cpp b/src/view/kateviewhelpers.cpp index d1527ded..9ed75f2b 100644 --- a/src/view/kateviewhelpers.cpp +++ b/src/view/kateviewhelpers.cpp @@ -1,3107 +1,3107 @@ /* This file is part of the KDE libraries Copyright (C) 2008, 2009 Matthew Woehlke Copyright (C) 2007 Mirko Stocker Copyright (C) 2002 John Firebaugh Copyright (C) 2001 Anders Lund Copyright (C) 2001 Christoph Cullmann Copyright (C) 2011 Svyatoslav Kuzmich Copyright (C) 2012 Kåre Särs (Minimap) Copyright 2017-2018 Friedrich W. H. Kossebau This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateviewhelpers.h" #include "katecmd.h" #include #include #include #include "kateconfig.h" #include "katedocument.h" #include #include "katerenderer.h" #include "kateannotationitemdelegate.h" #include "kateview.h" #include "kateviewinternal.h" #include "katelayoutcache.h" #include "katetextlayout.h" #include "kateglobal.h" #include "katepartdebug.h" #include "katecommandrangeexpressionparser.h" #include "kateabstractinputmode.h" #include "katetextpreview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //BEGIN KateMessageLayout KateMessageLayout::KateMessageLayout(QWidget *parent) : QLayout(parent) { } KateMessageLayout::~KateMessageLayout() { while (QLayoutItem *item = takeAt(0)) delete item; } void KateMessageLayout::addItem(QLayoutItem *item) { Q_ASSERT(false); add(item, KTextEditor::Message::CenterInView); } void KateMessageLayout::addWidget(QWidget *widget, KTextEditor::Message::MessagePosition pos) { add(new QWidgetItem(widget), pos); } int KateMessageLayout::count() const { return m_items.size(); } QLayoutItem *KateMessageLayout::itemAt(int index) const { if (index < 0 || index >= m_items.size()) return nullptr; return m_items[index]->item; } void KateMessageLayout::setGeometry(const QRect &rect) { QLayout::setGeometry(rect); const int s = spacing(); const QRect adjustedRect = rect.adjusted(s, s, -s, -s); for (auto wrapper : m_items) { QLayoutItem *item = wrapper->item; auto position = wrapper->position; if (position == KTextEditor::Message::TopInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), s, item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::BottomInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), adjustedRect.height() - item->sizeHint().height(), item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::CenterInView) { QRect r(0, 0, item->sizeHint().width(), item->sizeHint().height()); r.moveCenter(adjustedRect.center()); item->setGeometry(r); } else { Q_ASSERT_X(false, "setGeometry", "Only TopInView, CenterInView, and BottomInView are supported."); } } } QSize KateMessageLayout::sizeHint() const { return QSize(); } QLayoutItem *KateMessageLayout::takeAt(int index) { if (index >= 0 && index < m_items.size()) { ItemWrapper *layoutStruct = m_items.takeAt(index); return layoutStruct->item; } return nullptr; } void KateMessageLayout::add(QLayoutItem *item, KTextEditor::Message::MessagePosition pos) { m_items.push_back(new ItemWrapper(item, pos)); } //END KateMessageLayout //BEGIN KateScrollBar static const int s_lineWidth = 100; static const int s_pixelMargin = 8; static const int s_linePixelIncLimit = 6; const unsigned char KateScrollBar::characterOpacity[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 15 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, // <- 31 0, 125, 41, 221, 138, 195, 218, 21, 142, 142, 137, 137, 97, 87, 87, 140, // <- 47 223, 164, 183, 190, 191, 193, 214, 158, 227, 216, 103, 113, 146, 140, 146, 149, // <- 63 248, 204, 240, 174, 217, 197, 178, 205, 209, 176, 168, 211, 160, 246, 238, 218, // <- 79 195, 229, 227, 196, 167, 212, 188, 238, 197, 169, 189, 158, 21, 151, 115, 90, // <- 95 15, 192, 209, 153, 208, 187, 162, 221, 183, 149, 161, 191, 146, 203, 167, 182, // <- 111 208, 203, 139, 166, 158, 167, 157, 189, 164, 179, 156, 167, 145, 166, 109, 0, // <- 127 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 143 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 159 0, 125, 184, 187, 146, 201, 127, 203, 89, 194, 156, 141, 117, 87, 202, 88, // <- 175 115, 165, 118, 121, 85, 190, 236, 87, 88, 111, 151, 140, 194, 191, 203, 148, // <- 191 215, 215, 222, 224, 223, 234, 230, 192, 208, 208, 216, 217, 187, 187, 194, 195, // <- 207 228, 255, 228, 228, 235, 239, 237, 150, 255, 222, 222, 229, 232, 180, 197, 225, // <- 223 208, 208, 216, 217, 212, 230, 218, 170, 202, 202, 211, 204, 156, 156, 165, 159, // <- 239 214, 194, 197, 197, 206, 206, 201, 132, 214, 183, 183, 192, 187, 195, 227, 198 }; KateScrollBar::KateScrollBar(Qt::Orientation orientation, KateViewInternal *parent) : QScrollBar(orientation, parent->m_view) , m_middleMouseDown(false) , m_leftMouseDown(false) , m_view(parent->m_view) , m_doc(parent->doc()) , m_viewInternal(parent) , m_textPreview(nullptr) , m_showMarks(false) , m_showMiniMap(false) , m_miniMapAll(true) , m_miniMapWidth(40) , m_grooveHeight(height()) , m_linesModified(0) { connect(this, SIGNAL(valueChanged(int)), this, SLOT(sliderMaybeMoved(int))); connect(m_doc, SIGNAL(marksChanged(KTextEditor::Document*)), this, SLOT(marksChanged())); m_updateTimer.setInterval(300); m_updateTimer.setSingleShot(true); QTimer::singleShot(10, this, SLOT(updatePixmap())); // track mouse for text preview widget setMouseTracking(orientation == Qt::Vertical); // setup text preview timer m_delayTextPreviewTimer.setSingleShot(true); m_delayTextPreviewTimer.setInterval(250); connect(&m_delayTextPreviewTimer, SIGNAL(timeout()), this, SLOT(showTextPreview())); } KateScrollBar::~KateScrollBar() { delete m_textPreview; } void KateScrollBar::setShowMiniMap(bool b) { if (b && !m_showMiniMap) { connect(m_view, SIGNAL(selectionChanged(KTextEditor::View*)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_doc, SIGNAL(textChanged(KTextEditor::Document*)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_view, SIGNAL(delayedUpdateOfView()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updatePixmap()), Qt::UniqueConnection); connect(&(m_view->textFolding()), SIGNAL(foldingRangesChanged()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); } else if (!b) { disconnect(&m_updateTimer); } m_showMiniMap = b; updateGeometry(); update(); } QSize KateScrollBar::sizeHint() const { if (m_showMiniMap) { return QSize(m_miniMapWidth, QScrollBar::sizeHint().height()); } return QScrollBar::sizeHint(); } int KateScrollBar::minimapYToStdY(int y) { // Check if the minimap fills the whole scrollbar if (m_stdGroveRect.height() == m_mapGroveRect.height()) { return y; } // check if y is on the step up/down if ((y < m_stdGroveRect.top()) || (y > m_stdGroveRect.bottom())) { return y; } if (y < m_mapGroveRect.top()) { return m_stdGroveRect.top() + 1; } if (y > m_mapGroveRect.bottom()) { return m_stdGroveRect.bottom() - 1; } // check for div/0 if (m_mapGroveRect.height() == 0) { return y; } int newY = (y - m_mapGroveRect.top()) * m_stdGroveRect.height() / m_mapGroveRect.height(); newY += m_stdGroveRect.top(); return newY; } void KateScrollBar::mousePressEvent(QMouseEvent *e) { // delete text preview hideTextPreview(); if (e->button() == Qt::MidButton) { m_middleMouseDown = true; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = true; } if (m_showMiniMap) { if (m_leftMouseDown && e->pos().y() > m_mapGroveRect.top() && e->pos().y() < m_mapGroveRect.bottom()) { // if we show the minimap left-click jumps directly to the selected position int newVal = (e->pos().y()-m_mapGroveRect.top()) / (double)m_mapGroveRect.height() * (double)(maximum()+pageStep()) - pageStep()/2; newVal = qBound(0, newVal, maximum()); setSliderPosition(newVal); } QMouseEvent eMod(QEvent::MouseButtonPress, QPoint(6, minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mousePressEvent(&eMod); } else { QScrollBar::mousePressEvent(e); } m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); redrawMarks(); } void KateScrollBar::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MidButton) { m_middleMouseDown = false; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = false; } redrawMarks(); if (m_leftMouseDown || m_middleMouseDown) { QToolTip::hideText(); } if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseButtonRelease, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseReleaseEvent(&eMod); } else { QScrollBar::mouseReleaseEvent(e); } } void KateScrollBar::mouseMoveEvent(QMouseEvent *e) { if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseMove, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseMoveEvent(&eMod); } else { QScrollBar::mouseMoveEvent(e); } if (e->buttons() & (Qt::LeftButton | Qt::MidButton)) { redrawMarks(); // current line tool tip m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } showTextPreviewDelayed(); } void KateScrollBar::leaveEvent(QEvent *event) { hideTextPreview(); QAbstractSlider::leaveEvent(event); } bool KateScrollBar::eventFilter(QObject *object, QEvent *event) { Q_UNUSED(object) if (m_textPreview && event->type() == QEvent::WindowDeactivate) { // We need hide the scrollbar TextPreview widget hideTextPreview(); } return false; } void KateScrollBar::paintEvent(QPaintEvent *e) { if (m_doc->marks().size() != m_lines.size()) { recomputeMarksPositions(); } if (m_showMiniMap) { miniMapPaintEvent(e); } else { normalPaintEvent(e); } } void KateScrollBar::showTextPreviewDelayed() { if (!m_textPreview) { if (!m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.start(); } } else { showTextPreview(); } } void KateScrollBar::showTextPreview() { if (orientation() != Qt::Vertical || isSliderDown() || (minimum() == maximum()) || !m_view->config()->scrollBarPreview()) { return; } // only show when main window is active (#392396) if (window() && !window()->isActiveWindow()) { return; } QRect grooveRect; if (m_showMiniMap) { // If mini-map is shown, the height of the map might not be the whole height grooveRect = m_mapGroveRect; } else { QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); } if (m_view->config()->scrollPastEnd()) { // Adjust the grove size to accommodate the added pageStep at the bottom int adjust = pageStep()*grooveRect.height() / (maximum() + pageStep() - minimum()); grooveRect.adjust(0,0,0, -adjust); } const QPoint cursorPos = mapFromGlobal(QCursor::pos()); if (grooveRect.contains(cursorPos)) { if (!m_textPreview) { m_textPreview = new KateTextPreview(m_view, this); m_textPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_textPreview->setFrameStyle(QFrame::StyledPanel); // event filter to catch application WindowDeactivate event, to hide the preview window qApp->installEventFilter(this); } const qreal posInPercent = static_cast(cursorPos.y() - grooveRect.top()) / grooveRect.height(); const qreal startLine = posInPercent * m_view->textFolding().visibleLines(); m_textPreview->resize(m_view->width() / 2, m_view->height() / 5); const int xGlobal = mapToGlobal(QPoint(0, 0)).x(); const int yGlobal = qMin(mapToGlobal(QPoint(0, height())).y() - m_textPreview->height(), qMax(mapToGlobal(QPoint(0, 0)).y(), mapToGlobal(cursorPos).y() - m_textPreview->height() / 2)); m_textPreview->move(xGlobal - m_textPreview->width(), yGlobal); m_textPreview->setLine(startLine); m_textPreview->setCenterView(true); m_textPreview->setScaleFactor(0.8); m_textPreview->raise(); m_textPreview->show(); } else { hideTextPreview(); } } void KateScrollBar::hideTextPreview() { if (m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.stop(); } qApp->removeEventFilter(this); delete m_textPreview; } // This function is optimized for bing called in sequence. const QColor KateScrollBar::charColor(const QVector &attributes, int &attributeIndex, const QVector &decorations, const QColor &defaultColor, int x, QChar ch) { QColor color = defaultColor; bool styleFound = false; // Query the decorations, that is, things like search highlighting, or the // KDevelop DUChain highlighting, for a color to use foreach (const QTextLayout::FormatRange &range, decorations) { if (range.start <= x && range.start + range.length > x) { // If there's a different background color set (search markers, ...) // use that, otherwise use the foreground color. if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { color = range.format.background().color(); } else { color = range.format.foreground().color(); } styleFound = true; break; } } // If there's no decoration set for the current character (this will mostly be the case for // plain Kate), query the styles, that is, the default kate syntax highlighting. if (!styleFound) { // go to the block containing x while ((attributeIndex < attributes.size()) && ((attributes[attributeIndex].offset + attributes[attributeIndex].length) < x)) { ++attributeIndex; } if ((attributeIndex < attributes.size()) && (x < attributes[attributeIndex].offset + attributes[attributeIndex].length)) { color = m_view->renderer()->attribute(attributes[attributeIndex].attributeValue)->foreground().color(); } } // Query how much "blackness" the character has. // This causes for example a dot or a dash to appear less intense // than an A or similar. // This gives the pixels created a bit of structure, which makes it look more // like real text. color.setAlpha((ch.unicode() < 256) ? characterOpacity[ch.unicode()] : 222); return color; } void KateScrollBar::updatePixmap() { //QTime time; //time.start(); if (!m_showMiniMap) { // make sure no time is wasted if the option is disabled return; } // For performance reason, only every n-th line will be drawn if the widget is // sufficiently small compared to the amount of lines in the document. int docLineCount = m_view->textFolding().visibleLines(); int pixmapLineCount = docLineCount; if (m_view->config()->scrollPastEnd()) { pixmapLineCount += pageStep(); } int pixmapLinesUnscaled = pixmapLineCount; if (m_grooveHeight < 5) { m_grooveHeight = 5; } int lineDivisor = pixmapLinesUnscaled / m_grooveHeight; if (lineDivisor < 1) { lineDivisor = 1; } int charIncrement = 1; int lineIncrement = 1; if ((m_grooveHeight > 10) && (pixmapLineCount >= m_grooveHeight * 2)) { charIncrement = pixmapLineCount / m_grooveHeight; while (charIncrement > s_linePixelIncLimit) { lineIncrement++; pixmapLineCount = pixmapLinesUnscaled / lineIncrement; charIncrement = pixmapLineCount / m_grooveHeight; } pixmapLineCount /= charIncrement; } int pixmapLineWidth = s_pixelMargin + s_lineWidth / charIncrement; //qCDebug(LOG_KTE) << "l" << lineIncrement << "c" << charIncrement << "d" << lineDivisor; //qCDebug(LOG_KTE) << "pixmap" << pixmapLineCount << pixmapLineWidth << "docLines" << m_view->textFolding().visibleLines() << "height" << m_grooveHeight; const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor defaultTextColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor selectionBgColor = m_view->renderer()->config()->selectionColor(); QColor modifiedLineColor = m_view->renderer()->config()->modifiedLineColor(); QColor savedLineColor = m_view->renderer()->config()->savedLineColor(); // move the modified line color away from the background color modifiedLineColor.setHsv(modifiedLineColor.hue(), 255, 255 - backgroundColor.value() / 3); savedLineColor.setHsv(savedLineColor.hue(), 100, 255 - backgroundColor.value() / 3); // increase dimensions by ratio m_pixmap = QPixmap(pixmapLineWidth * m_view->devicePixelRatioF(), pixmapLineCount * m_view->devicePixelRatioF()); m_pixmap.fill(QColor("transparent")); // The text currently selected in the document, to be drawn later. const KTextEditor::Range &selection = m_view->selectionRange(); QPainter painter; if (painter.begin(&m_pixmap)) { // init pen once, afterwards, only change it if color changes to avoid a lot of allocation for setPen painter.setPen(selectionBgColor); // Do not force updates of the highlighting if the document is very large bool simpleMode = m_doc->lines() > 7500; int pixelY = 0; int drawnLines = 0; // Iterate over all visible lines, drawing them. for (int virtualLine = 0; virtualLine < docLineCount; virtualLine += lineIncrement) { int realLineNumber = m_view->textFolding().visibleLineToLine(virtualLine); QString lineText = m_doc->line(realLineNumber); if (!simpleMode) { m_doc->buffer().ensureHighlighted(realLineNumber); } const Kate::TextLine &kateline = m_doc->plainKateTextLine(realLineNumber); const QVector &attributes = kateline->attributesList(); QVector decorations = m_view->renderer()->decorationsForLine(kateline, realLineNumber); int attributeIndex = 0; // Draw selection if it is on an empty line if (selection.contains(KTextEditor::Cursor(realLineNumber, 0)) && lineText.size() == 0) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(s_pixelMargin, pixelY, s_pixelMargin + s_lineWidth - 1, pixelY); } // Iterate over the line to draw the background int selStartX = -1; int selEndX = -1; int pixelX = s_pixelMargin; // use this to control the offset of the text from the left for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // Query the selection and draw it behind the character if (selection.contains(KTextEditor::Cursor(realLineNumber, x))) { if (selStartX == -1) selStartX = pixelX; selEndX = pixelX; if (lineText.size() - 1 == x) { selEndX = s_lineWidth + s_pixelMargin-1; } } if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { pixelX++; } } if (selStartX != -1) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(selStartX, pixelY, selEndX, pixelY); } // Iterate over all the characters in the current line pixelX = s_pixelMargin; for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // draw the pixels if (lineText[x] == QLatin1Char(' ')) { pixelX++; } else if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { const QColor newPenColor(charColor(attributes, attributeIndex, decorations, defaultTextColor, x, lineText[x])); if (newPenColor != painter.pen().color()) { painter.setPen(newPenColor); } // Actually draw the pixel with the color queried from the renderer. painter.drawPoint(pixelX, pixelY); pixelX++; } } drawnLines++; if (((drawnLines) % charIncrement) == 0) { pixelY++; } } //qCDebug(LOG_KTE) << drawnLines; // Draw line modification marker map. // Disable this if the document is really huge, // since it requires querying every line. if (m_doc->lines() < 50000) { for (int lineno = 0; lineno < docLineCount; lineno++) { int realLineNo = m_view->textFolding().visibleLineToLine(lineno); const Kate::TextLine &line = m_doc->plainKateTextLine(realLineNo); const QColor & col = line->markedAsModified() ? modifiedLineColor : savedLineColor; if (line->markedAsModified() || line->markedAsSavedOnDisk()) { painter.fillRect(2, lineno / lineDivisor, 3, 1, col); } } } // end painting painter.end(); } // set right ratio m_pixmap.setDevicePixelRatio(m_view->devicePixelRatioF()); //qCDebug(LOG_KTE) << time.elapsed(); // Redraw the scrollbar widget with the updated pixmap. update(); } void KateScrollBar::miniMapPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); m_stdGroveRect = grooveRect; if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSubLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.moveTop(alignMargin); grooveRect.setHeight(grooveRect.height() - alignMargin); } if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarAddLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.setHeight(grooveRect.height() - alignMargin); } m_grooveHeight = grooveRect.height(); const int docXMargin = 1; //style()->drawControl(QStyle::CE_ScrollBarAddLine, &opt, &painter, this); //style()->drawControl(QStyle::CE_ScrollBarSubLine, &opt, &painter, this); // calculate the document size and position const int docHeight = qMin(grooveRect.height(), int(m_pixmap.height() / m_pixmap.devicePixelRatio() * 2)) - 2 * docXMargin; const int yoffset = 1; // top-aligned in stead of center-aligned (grooveRect.height() - docHeight) / 2; const QRect docRect(QPoint(grooveRect.left() + docXMargin, yoffset + grooveRect.top()), QSize(grooveRect.width() - docXMargin, docHeight)); m_mapGroveRect = docRect; // calculate the visible area int max = qMax(maximum() + 1, 1); int visibleStart = value() * docHeight / (max + pageStep()) + docRect.top() + 0.5; int visibleEnd = (value() + pageStep()) * docHeight / (max + pageStep()) + docRect.top(); QRect visibleRect = docRect; visibleRect.moveTop(visibleStart); visibleRect.setHeight(visibleEnd - visibleStart); // calculate colors const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor foregroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor highlightColor = palette().link().color(); const int backgroundLightness = backgroundColor.lightness(); const int foregroundLightness = foregroundColor.lightness(); const int lighnessDiff = (foregroundLightness - backgroundLightness); // get a color suited for the color theme QColor darkShieldColor = palette().color(QPalette::Mid); int hue, sat, light; darkShieldColor.getHsl(&hue, &sat, &light); // apply suitable lightness darkShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.35); // gradient for nicer results QLinearGradient gradient(0, 0, width(), 0); gradient.setColorAt(0, darkShieldColor); gradient.setColorAt(0.3, darkShieldColor.lighter(115)); gradient.setColorAt(1, darkShieldColor); QColor lightShieldColor; lightShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.15); QColor outlineColor; outlineColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.5); // draw the grove background in case the document is small painter.setPen(Qt::NoPen); painter.setBrush(backgroundColor); painter.drawRect(grooveRect); // adjust the rectangles QRect sliderRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); sliderRect.setX(docXMargin); sliderRect.setWidth(width() - docXMargin*2); if ((docHeight + 2 * docXMargin >= grooveRect.height()) && (sliderRect.height() > visibleRect.height() + 2)) { visibleRect.adjust(2, 0, -3, 0); } else { visibleRect.adjust(1, 0, -1, 2); sliderRect.setTop(visibleRect.top() - 1); sliderRect.setBottom(visibleRect.bottom() + 1); } // Smooth transform only when squeezing if (grooveRect.height() < m_pixmap.height() / m_pixmap.devicePixelRatio()) { painter.setRenderHint(QPainter::SmoothPixmapTransform); } // draw the modified lines margin QRect pixmapMarginRect(QPoint(0, 0), QSize(s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapMarginRect(QPoint(0, docRect.top()), QSize(s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapMarginRect, m_pixmap, pixmapMarginRect); // calculate the stretch and draw the stretched lines (scrollbar marks) QRect pixmapRect(QPoint(s_pixelMargin, 0), QSize(m_pixmap.width() / m_pixmap.devicePixelRatio() - s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapRect(QPoint(s_pixelMargin, docRect.top()), QSize(docRect.width() - s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapRect, m_pixmap, pixmapRect); // delimit the end of the document const int y = docPixmapRect.height() + grooveRect.y(); if (y+2 < grooveRect.y() + grooveRect.height()) { QColor fg(foregroundColor); fg.setAlpha(30); painter.setBrush(Qt::NoBrush); painter.setPen(QPen(fg, 1)); painter.drawLine(grooveRect.x()+1,y+2,width()-1,y+2); } // fade the invisible sections const QRect top( grooveRect.x(), grooveRect.y(), grooveRect.width(), visibleRect.y()-grooveRect.y() //Pen width ); const QRect bottom( grooveRect.x(), grooveRect.y()+visibleRect.y()+visibleRect.height()-grooveRect.y(), //Pen width grooveRect.width(), grooveRect.height() - (visibleRect.y()-grooveRect.y())-visibleRect.height() ); QColor faded(backgroundColor); faded.setAlpha(110); painter.fillRect(top, faded); painter.fillRect(bottom, faded); // add a thin line to limit the scrollbar QColor c(foregroundColor); c.setAlpha(10); painter.setPen(QPen(c,1)); painter.drawLine(0, 0, 0, height()); if (m_showMarks) { QHashIterator it = m_lines; QPen penBg; penBg.setWidth(4); lightShieldColor.setAlpha(180); penBg.setColor(lightShieldColor); painter.setPen(penBg); while (it.hasNext()) { it.next(); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top();; painter.drawLine(6, y, width() - 6, y); } it = m_lines; QPen pen; pen.setWidth(2); while (it.hasNext()) { it.next(); pen.setColor(it.value()); painter.setPen(pen); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top(); painter.drawLine(6, y, width() - 6, y); } } // slider outline QColor sliderColor(highlightColor); sliderColor.setAlpha(50); painter.fillRect(sliderRect, sliderColor); painter.setPen(QPen(highlightColor, 0)); // rounded rect looks ugly for some reason, so we draw 4 lines. painter.drawLine(sliderRect.left(), sliderRect.top() + 1, sliderRect.left(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.right(), sliderRect.top() + 1, sliderRect.right(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.left() + 1, sliderRect.top(), sliderRect.right() - 1, sliderRect.top()); painter.drawLine(sliderRect.left() + 1, sliderRect.bottom(), sliderRect.right() - 1, sliderRect.bottom()); } void KateScrollBar::normalPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); if (!m_showMarks) { return; } QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect rect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); int sideMargin = width() - rect.width(); if (sideMargin < 4) { sideMargin = 4; } sideMargin /= 2; QHashIterator it = m_lines; while (it.hasNext()) { it.next(); painter.setPen(it.value()); if (it.key() < rect.top() || it.key() > rect.bottom()) { painter.drawLine(0, it.key(), width(), it.key()); } else { painter.drawLine(0, it.key(), sideMargin, it.key()); painter.drawLine(width() - sideMargin, it.key(), width(), it.key()); } } } void KateScrollBar::resizeEvent(QResizeEvent *e) { QScrollBar::resizeEvent(e); m_updateTimer.start(); m_lines.clear(); update(); } void KateScrollBar::sliderChange(SliderChange change) { // call parents implementation QScrollBar::sliderChange(change); if (change == QAbstractSlider::SliderValueChange) { redrawMarks(); } else if (change == QAbstractSlider::SliderRangeChange) { marksChanged(); } if (m_leftMouseDown || m_middleMouseDown) { const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } } void KateScrollBar::marksChanged() { m_lines.clear(); update(); } void KateScrollBar::redrawMarks() { if (!m_showMarks) { return; } update(); } void KateScrollBar::recomputeMarksPositions() { // get the style options to compute the scrollbar pixels QStyleOptionSlider opt; initStyleOption(&opt); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); // cache top margin and groove height const int top = grooveRect.top(); const int h = grooveRect.height() - 1; // make sure we have a sane height if (h <= 0) { return; } // get total visible (=without folded) lines in the document int visibleLines = m_view->textFolding().visibleLines() - 1; if (m_view->config()->scrollPastEnd()) { visibleLines += m_viewInternal->linesDisplayed() - 1; visibleLines -= m_view->config()->autoCenterLines(); } // now repopulate the scrollbar lines list m_lines.clear(); const QHash &marks = m_doc->marks(); for (QHash::const_iterator i = marks.constBegin(); i != marks.constEnd(); ++i) { KTextEditor::Mark *mark = i.value(); const int line = m_view->textFolding().lineToVisibleLine(mark->line); const double ratio = static_cast(line) / visibleLines; m_lines.insert(top + (int)(h * ratio), KateRendererConfig::global()->lineMarkerColor((KTextEditor::MarkInterface::MarkTypes)mark->type)); } } void KateScrollBar::sliderMaybeMoved(int value) { if (m_middleMouseDown) { // we only need to emit this signal once, as for the following slider // movements the signal sliderMoved() is already emitted. // Thus, set m_middleMouseDown to false right away. m_middleMouseDown = false; emit sliderMMBMoved(value); } } //END //BEGIN KateCmdLineEditFlagCompletion /** * This class provide completion of flags. It shows a short description of * each flag, and flags are appended. */ class KateCmdLineEditFlagCompletion : public KCompletion { public: KateCmdLineEditFlagCompletion() { ; } QString makeCompletion(const QString & /*s*/) override { return QString(); } }; //END KateCmdLineEditFlagCompletion //BEGIN KateCmdLineEdit KateCommandLineBar::KateCommandLineBar(KTextEditor::ViewPrivate *view, QWidget *parent) : KateViewBarWidget(true, parent) { QHBoxLayout *topLayout = new QHBoxLayout(); centralWidget()->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); m_lineEdit = new KateCmdLineEdit(this, view); connect(m_lineEdit, SIGNAL(hideRequested()), SIGNAL(hideMe())); topLayout->addWidget(m_lineEdit); QToolButton *helpButton = new QToolButton(this); helpButton->setAutoRaise(true); helpButton->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); topLayout->addWidget(helpButton); connect(helpButton, SIGNAL(clicked()), this, SLOT(showHelpPage())); setFocusProxy(m_lineEdit); } void KateCommandLineBar::showHelpPage() { KHelpClient::invokeHelp(QStringLiteral("advanced-editing-tools-commandline"), QStringLiteral("kate")); } KateCommandLineBar::~KateCommandLineBar() { } // inserts the given string in the command line edit and (if selected = true) selects it so the user // can type over it if they want to void KateCommandLineBar::setText(const QString &text, bool selected) { m_lineEdit->setText(text); if (selected) { m_lineEdit->selectAll(); } } void KateCommandLineBar::execute(const QString &text) { m_lineEdit->slotReturnPressed(text); } KateCmdLineEdit::KateCmdLineEdit(KateCommandLineBar *bar, KTextEditor::ViewPrivate *view) : KLineEdit() , m_view(view) , m_bar(bar) , m_msgMode(false) , m_histpos(0) , m_cmdend(0) , m_command(nullptr) { connect(this, SIGNAL(returnPressed(QString)), this, SLOT(slotReturnPressed(QString))); setCompletionObject(KateCmd::self()->commandCompletionObject()); setAutoDeleteCompletionObject(false); m_hideTimer = new QTimer(this); m_hideTimer->setSingleShot(true); connect(m_hideTimer, SIGNAL(timeout()), this, SLOT(hideLineEdit())); // make sure the timer is stopped when the user switches views. if not, focus will be given to the // wrong view when KateViewBar::hideCurrentBarWidget() is called after 4 seconds. (the timer is // used for showing things like "Success" for four seconds after the user has used the kate // command line) connect(m_view, SIGNAL(focusOut(KTextEditor::View*)), m_hideTimer, SLOT(stop())); } void KateCmdLineEdit::hideEvent(QHideEvent *e) { Q_UNUSED(e); } QString KateCmdLineEdit::helptext(const QPoint &) const { const QString beg = QStringLiteral("
Help: "); const QString mid = QStringLiteral("
"); const QString end = QStringLiteral("
"); const QString t = text(); - static const QRegularExpression re(QLatin1String("\\s*help\\s+(.*)")); + static const QRegularExpression re(QStringLiteral("\\s*help\\s+(.*)")); auto match = re.match(t); if (match.hasMatch()) { QString s; // get help for command const QString name = match.captured(1); if (name == QLatin1String("list")) { return beg + i18n("Available Commands") + mid + KateCmd::self()->commandList().join(QLatin1Char(' ')) + i18n("

For help on individual commands, do 'help <command>'

") + end; } else if (! name.isEmpty()) { KTextEditor::Command *cmd = KateCmd::self()->queryCommand(name); if (cmd) { if (cmd->help(m_view, name, s)) { return beg + name + mid + s + end; } else { return beg + name + mid + i18n("No help for '%1'", name) + end; } } else { return beg + mid + i18n("No such command %1", name) + end; } } } return beg + mid + i18n( "

This is the Katepart command line.
" "Syntax: command [ arguments ]
" "For a list of available commands, enter help list
" "For help for individual commands, enter help <command>

") + end; } bool KateCmdLineEdit::event(QEvent *e) { if (e->type() == QEvent::QueryWhatsThis) { setWhatsThis(helptext(QPoint())); e->accept(); return true; } return KLineEdit::event(e); } /** * Parse the text as a command. * * The following is a simple PEG grammar for the syntax of the command. * (A PEG grammar is like a BNF grammar, except that "/" stands for * ordered choice: only the first matching rule is used. In other words, * the parsing is short-circuited in the manner of the "or" operator in * programming languages, and so the grammar is unambiguous.) * * Text <- Range? Command * / Position * Range <- Position ("," Position)? * / "%" * Position <- Base Offset? * Base <- Line * / LastLine * / ThisLine * / Mark * Offset <- [+-] Base * Line <- [0-9]+ * LastLine <- "$" * ThisLine <- "." * Mark <- "'" [a-z] */ void KateCmdLineEdit::slotReturnPressed(const QString &text) { if (text.isEmpty()) { return; } // silently ignore leading space characters uint n = 0; const uint textlen = text.length(); while ((n < textlen) && (text[n].isSpace())) { n++; } if (n >= textlen) { return; } QString cmd = text.mid(n); // Parse any leading range expression, and strip it (and maybe do some other transforms on the command). QString leadingRangeExpression; KTextEditor::Range range = CommandRangeExpressionParser::parseRangeExpression(cmd, m_view, leadingRangeExpression, cmd); // Built in help: if the command starts with "help", [try to] show some help if (cmd.startsWith(QLatin1String("help"))) { QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), helptext(QPoint())); clear(); KateCmd::self()->appendHistory(cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); return; } if (cmd.length() > 0) { KTextEditor::Command *p = KateCmd::self()->queryCommand(cmd); m_oldText = leadingRangeExpression + cmd; m_msgMode = true; // the following commands changes the focus themselves, so bar should be hidden before execution. - if (QRegularExpression(QLatin1String("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { + if (QRegularExpression(QStringLiteral("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { emit hideRequested(); } if (!p) { setText(i18n("No such command: \"%1\"", cmd)); } else if (range.isValid() && !p->supportsRange(cmd)) { // Raise message, when the command does not support ranges. setText(i18n("Error: No range allowed for command \"%1\".", cmd)); } else { QString msg; if (p->exec(m_view, cmd, msg, range)) { // append command along with range (will be empty if none given) to history KateCmd::self()->appendHistory(leadingRangeExpression + cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); if (msg.length() > 0) { setText(i18n("Success: ") + msg); } else if (isVisible()) { // always hide on success without message emit hideRequested(); } } else { if (msg.length() > 0) { if (msg.contains(QLatin1Char('\n'))) { // multiline error, use widget with more space QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), msg); } else { setText(msg); } } else { setText(i18n("Command \"%1\" failed.", cmd)); } } } } // clean up if (completionObject() != KateCmd::self()->commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_command = nullptr; m_cmdend = 0; // the following commands change the focus themselves - if (!QRegularExpression(QLatin1String("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { + if (!QRegularExpression(QStringLiteral("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")).match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { m_view->setFocus(); } if (isVisible()) { m_hideTimer->start(4000); } } void KateCmdLineEdit::hideLineEdit() // unless i have focus ;) { if (! hasFocus()) { emit hideRequested(); } } void KateCmdLineEdit::focusInEvent(QFocusEvent *ev) { if (m_msgMode) { m_msgMode = false; setText(m_oldText); selectAll(); } KLineEdit::focusInEvent(ev); } void KateCmdLineEdit::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Escape || (ev->key() == Qt::Key_BracketLeft && ev->modifiers() == Qt::ControlModifier)) { m_view->setFocus(); hideLineEdit(); clear(); } else if (ev->key() == Qt::Key_Up) { fromHistory(true); } else if (ev->key() == Qt::Key_Down) { fromHistory(false); } uint cursorpos = cursorPosition(); KLineEdit::keyPressEvent(ev); // during typing, let us see if we have a valid command if (! m_cmdend || cursorpos <= m_cmdend) { QChar c; if (! ev->text().isEmpty()) { c = ev->text().at(0); } if (! m_cmdend && ! c.isNull()) { // we have no command, so lets see if we got one if (! c.isLetterOrNumber() && c != QLatin1Char('-') && c != QLatin1Char('_')) { m_command = KateCmd::self()->queryCommand(text().trimmed()); if (m_command) { //qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<queryCommand(text().trimmed()); if (m_command) { //qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_cmdend = 0; } } // if we got a command, check if it wants to do something. if (m_command) { KCompletion *cmpl = m_command->completionObject(m_view, text().left(m_cmdend).trimmed()); if (cmpl) { // We need to prepend the current command name + flag string // when completion is done //qCDebug(LOG_KTE)<<"keypress in commandline: Setting completion object!"; setCompletionObject(cmpl); } } } else if (m_command && !ev->text().isEmpty()) { // check if we should call the commands processText() if (m_command->wantsToProcessText(text().left(m_cmdend).trimmed())) { m_command->processText(m_view, text()); } } } void KateCmdLineEdit::fromHistory(bool up) { if (! KateCmd::self()->historyLength()) { return; } QString s; if (up) { if (m_histpos > 0) { m_histpos--; s = KateCmd::self()->fromHistory(m_histpos); } } else { if (m_histpos < (KateCmd::self()->historyLength() - 1)) { m_histpos++; s = KateCmd::self()->fromHistory(m_histpos); } else { m_histpos = KateCmd::self()->historyLength(); setText(m_oldText); } } if (! s.isEmpty()) { // Select the argument part of the command, so that it is easy to overwrite setText(s); - static const QRegularExpression reCmd(QLatin1String("^[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)")); + static const QRegularExpression reCmd(QStringLiteral("^[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)")); const auto match = reCmd.match(text()); if (match.hasMatch()) { setSelection(text().length() - match.capturedLength(1), match.capturedLength(1)); } } } //END KateCmdLineEdit //BEGIN KateIconBorder using namespace KTextEditor; KateIconBorder::KateIconBorder(KateViewInternal *internalView, QWidget *parent) : QWidget(parent) , m_view(internalView->m_view) , m_doc(internalView->doc()) , m_viewInternal(internalView) , m_iconBorderOn(false) , m_lineNumbersOn(false) , m_relLineNumbersOn(false) , m_updateRelLineNumbers(false) , m_foldingMarkersOn(false) , m_dynWrapIndicatorsOn(false) , m_annotationBorderOn(false) , m_updatePositionToArea(true) , m_annotationItemDelegate(new KateAnnotationItemDelegate(m_viewInternal, this)) { setAcceptDrops(true); setAttribute(Qt::WA_StaticContents); // See: https://doc.qt.io/qt-5/qwidget.html#update. As this widget does not // have a background, there's no need for Qt to erase the widget's area // before repainting. Enabling this prevents flickering when the widget is // repainted. setAttribute(Qt::WA_OpaquePaintEvent); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); setMouseTracking(true); m_doc->setMarkDescription(MarkInterface::markType01, i18n("Bookmark")); m_doc->setMarkPixmap(MarkInterface::markType01, QIcon::fromTheme(QStringLiteral("bookmarks")).pixmap(32, 32)); connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); updateFont(); m_antiFlickerTimer.setSingleShot(true); m_antiFlickerTimer.setInterval(300); connect(&m_antiFlickerTimer, &QTimer::timeout, this, &KateIconBorder::highlightFolding); // user interaction (scrolling) hides e.g. preview connect(m_view, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate*)), this, SLOT(displayRangeChanged())); } KateIconBorder::~KateIconBorder() { delete m_foldingPreview; delete m_foldingRange; } void KateIconBorder::setIconBorderOn(bool enable) { if (enable == m_iconBorderOn) { return; } m_iconBorderOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setAnnotationBorderOn(bool enable) { if (enable == m_annotationBorderOn) { return; } m_annotationBorderOn = enable; // make sure the tooltip is hidden if (!m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } emit m_view->annotationBorderVisibilityChanged(m_view, enable); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::removeAnnotationHovering() { // remove hovering if it's still there if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::setLineNumbersOn(bool enable) { if (enable == m_lineNumbersOn) { return; } m_lineNumbersOn = enable; m_dynWrapIndicatorsOn = (m_dynWrapIndicators == 1) ? enable : m_dynWrapIndicators; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setRelLineNumbersOn(bool enable) { if (enable == m_relLineNumbersOn) { return; } m_relLineNumbersOn = enable; /* * We don't have to touch the m_dynWrapIndicatorsOn because * we already got it right from the m_lineNumbersOn */ m_updatePositionToArea = true; QTimer::singleShot( 0, this, SLOT(update()) ); } void KateIconBorder::updateForCursorLineChange() { if (m_relLineNumbersOn) { m_updateRelLineNumbers = true; } // always do normal update, e.g. for different current line color! update(); } void KateIconBorder::setDynWrapIndicators(int state) { if (state == m_dynWrapIndicators) { return; } m_dynWrapIndicators = state; m_dynWrapIndicatorsOn = (state == 1) ? m_lineNumbersOn : state; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setFoldingMarkersOn(bool enable) { if (enable == m_foldingMarkersOn) { return; } m_foldingMarkersOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } QSize KateIconBorder::sizeHint() const { int w = 1; // Must be any value != 0 or we will never painted! const int i = m_positionToArea.size(); if (i > 0) { w = m_positionToArea.at(i - 1).first; } return QSize(w, 0); } // This function (re)calculates the maximum width of any of the digit characters (0 -> 9) // for graceful handling of variable-width fonts as the linenumber font. void KateIconBorder::updateFont() { const QFontMetricsF &fm = m_view->renderer()->config()->fontMetrics(); m_maxCharWidth = 0.0; // Loop to determine the widest numeric character in the current font. // 48 is ascii '0' for (int i = 48; i < 58; i++) { const qreal charWidth = ceil(fm.width(QChar(i))); m_maxCharWidth = qMax(m_maxCharWidth, charWidth); } // NOTE/TODO(or not) Take size of m_dynWrapIndicatorChar into account. // It's a multi-char and it's reported size is, even with a mono-space font, // bigger than each digit, e.g. 10 vs 12. Currently it seems to work even with // "Line Numbers Off" but all these width calculating looks slightly hacky // the icon pane scales with the font... m_iconAreaWidth = fm.height(); // Only for now, later may that become an own value m_foldingAreaWidth = m_iconAreaWidth; calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } int KateIconBorder::lineNumberWidth() const { int width = 0; // Avoid unneeded expensive calculations ;-) if (m_lineNumbersOn) { // width = (number of digits + 1) * char width const int digits = (int) ceil(log10((double)(m_view->doc()->lines() + 1))); width = (int)ceil((digits + 1) * m_maxCharWidth); } if ((width < 1) && m_dynWrapIndicatorsOn && m_view->config()->dynWordWrap()) { // FIXME Why 2x? because of above (number of digits + 1) // -> looks to me like a hint for bad calculation elsewhere width = 2 * m_maxCharWidth; } return width; } void KateIconBorder::dragMoveEvent(QDragMoveEvent *event) { // FIXME Just calling m_view->m_viewInternal->dragMoveEvent(e) don't work // as intended, we need to set the cursor at column 1 // Is there a way to change the pos of the event? QPoint pos(0, event->pos().y()); // Code copy of KateViewInternal::dragMoveEvent m_view->m_viewInternal->placeCursor(pos, true, false); m_view->m_viewInternal->fixDropEvent(event); } void KateIconBorder::dropEvent(QDropEvent *event) { m_view->m_viewInternal->dropEvent(event); } void KateIconBorder::paintEvent(QPaintEvent *e) { paintBorder(e->rect().x(), e->rect().y(), e->rect().width(), e->rect().height()); } static void paintTriangle(QPainter &painter, QColor c, int xOffset, int yOffset, int width, int height, bool open) { painter.setRenderHint(QPainter::Antialiasing); qreal size = qMin(width, height); if (open) { // Paint unfolded icon less pushy if (KColorUtils::luma(c) < 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } else { // Paint folded icon in contrast to popup highlighting if (KColorUtils::luma(c) > 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } QPen pen; pen.setJoinStyle(Qt::RoundJoin); pen.setColor(c); pen.setWidthF(1.5); painter.setPen(pen); painter.setBrush(c); // let some border, if possible size *= 0.6; qreal halfSize = size / 2; qreal halfSizeP = halfSize * 0.6; QPointF middle(xOffset + (qreal)width / 2, yOffset + (qreal)height / 2); if (open) { QPointF points[3] = { middle + QPointF(-halfSize, -halfSizeP), middle + QPointF(halfSize, -halfSizeP), middle + QPointF(0, halfSizeP) }; painter.drawConvexPolygon(points, 3); } else { QPointF points[3] = { middle + QPointF(-halfSizeP, -halfSize), middle + QPointF(-halfSizeP, halfSize), middle + QPointF(halfSizeP, 0) }; painter.drawConvexPolygon(points, 3); } painter.setRenderHint(QPainter::Antialiasing, false); } /** * Helper class for an identifier which can be an empty or non-empty string or invalid. * Avoids complicated explicit statements in code dealing with the identifier * received as QVariant from a model. */ class KateAnnotationGroupIdentifier { public: KateAnnotationGroupIdentifier(const QVariant &identifier) : m_isValid(identifier.isValid() && identifier.canConvert()) , m_id(m_isValid ? identifier.toString() : QString()) { } KateAnnotationGroupIdentifier() = default; KateAnnotationGroupIdentifier(const KateAnnotationGroupIdentifier &rhs) = default; KateAnnotationGroupIdentifier& operator=(const KateAnnotationGroupIdentifier &rhs) { m_isValid = rhs.m_isValid; m_id = rhs.m_id; return *this; } KateAnnotationGroupIdentifier& operator=(const QVariant &identifier) { m_isValid = (identifier.isValid() && identifier.canConvert()); if (m_isValid) { m_id = identifier.toString(); } else { m_id.clear(); } return *this; } bool operator==(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid == rhs.m_isValid) && (!m_isValid || (m_id == rhs.m_id)); } bool operator!=(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid != rhs.m_isValid) || (m_isValid && (m_id != rhs.m_id)); } void clear() { m_isValid = false; m_id.clear(); } bool isValid() const { return m_isValid; } const QString& id() const { return m_id; } private: bool m_isValid = false; QString m_id; }; /** * Helper class for iterative calculation of data regarding the position * of a line with regard to annotation item grouping. */ class KateAnnotationGroupPositionState { public: /** * @param startz first rendered displayed line * @param isUsed flag whether the KateAnnotationGroupPositionState object will * be used or is just created due to being on the stack */ KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed); /** * @param styleOption option to fill with data for the given line * @param z rendered displayed line * @param realLine real line which is rendered here (passed to avoid another look-up) */ void nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine); private: KateViewInternal *m_viewInternal; const KTextEditor::AnnotationModel * const m_model; const QString m_hoveredAnnotationGroupIdentifier; int m_visibleWrappedLineInAnnotationGroup = -1; KateAnnotationGroupIdentifier m_lastAnnotationGroupIdentifier; KateAnnotationGroupIdentifier m_nextAnnotationGroupIdentifier; bool m_isSameAnnotationGroupsSinceLast = false; }; KateAnnotationGroupPositionState::KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed) : m_viewInternal(viewInternal) , m_model(model) , m_hoveredAnnotationGroupIdentifier(hoveredAnnotationGroupIdentifier) { if (!isUsed) { return; } if (!m_model || (static_cast(startz) >= m_viewInternal->cache()->viewCacheLineCount())) { return; } const auto realLineAtStart = m_viewInternal->cache()->viewLine(startz).line(); m_nextAnnotationGroupIdentifier = m_model->data(realLineAtStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_nextAnnotationGroupIdentifier.isValid()) { // estimate state of annotation group before first rendered line if (startz == 0) { if (realLineAtStart > 0) { // TODO: here we would want to scan until the next line that would be displayed, // to see if there are any group changes until then // for now simply taking neighbour line into account, not a grave bug on the first displayed line m_lastAnnotationGroupIdentifier = m_model->data(realLineAtStart - 1, (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole); m_isSameAnnotationGroupsSinceLast = (m_lastAnnotationGroupIdentifier == m_nextAnnotationGroupIdentifier); } } else { const auto realLineBeforeStart = m_viewInternal->cache()->viewLine(startz-1).line(); m_lastAnnotationGroupIdentifier = m_model->data(realLineBeforeStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_lastAnnotationGroupIdentifier.isValid()) { if (m_lastAnnotationGroupIdentifier.id() == m_nextAnnotationGroupIdentifier.id()) { m_isSameAnnotationGroupsSinceLast = true; // estimate m_visibleWrappedLineInAnnotationGroup from lines before startz for (uint z = startz; z > 0; --z) { const auto realLine = m_viewInternal->cache()->viewLine(z-1).line(); const KateAnnotationGroupIdentifier identifier = m_model->data(realLine, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (identifier != m_lastAnnotationGroupIdentifier) { break; } ++m_visibleWrappedLineInAnnotationGroup; } } } } } } void KateAnnotationGroupPositionState::nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine) { styleOption.wrappedLine = m_viewInternal->cache()->viewLine(z).viewLine(); styleOption.wrappedLineCount = m_viewInternal->cache()->viewLineCount(realLine); // Estimate position in group const KateAnnotationGroupIdentifier annotationGroupIdentifier = m_nextAnnotationGroupIdentifier; bool isSameAnnotationGroupsSinceThis = false; // Calculate next line's group identifier // shortcut: assuming wrapped lines are always displayed together, test is simple if (styleOption.wrappedLine+1 < styleOption.wrappedLineCount) { m_nextAnnotationGroupIdentifier = annotationGroupIdentifier; isSameAnnotationGroupsSinceThis = true; } else { if (static_cast(z+1) < m_viewInternal->cache()->viewCacheLineCount()) { const int realLineAfter = m_viewInternal->cache()->viewLine(z+1).line(); // search for any realLine with a different group id, also the non-displayed int rl = realLine + 1; for (; rl <= realLineAfter; ++rl) { m_nextAnnotationGroupIdentifier = m_model->data(rl, (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole); if (!m_nextAnnotationGroupIdentifier.isValid() || (m_nextAnnotationGroupIdentifier.id() != annotationGroupIdentifier.id())) { break; } } isSameAnnotationGroupsSinceThis = (rl > realLineAfter); if (rl < realLineAfter) { m_nextAnnotationGroupIdentifier = m_model->data(realLineAfter, (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole); } } else { // TODO: check next line after display end m_nextAnnotationGroupIdentifier.clear(); isSameAnnotationGroupsSinceThis = false; } } if (annotationGroupIdentifier.isValid()) { if (m_hoveredAnnotationGroupIdentifier == annotationGroupIdentifier.id()) { styleOption.state |= QStyle::State_MouseOver; } else { styleOption.state &= ~QStyle::State_MouseOver; } if (m_isSameAnnotationGroupsSinceLast) { ++m_visibleWrappedLineInAnnotationGroup; } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.annotationItemGroupingPosition = StyleOptionAnnotationItem::InGroup; if (!m_isSameAnnotationGroupsSinceLast) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupBegin; } if (!isSameAnnotationGroupsSinceThis) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupEnd; } } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.visibleWrappedLineInGroup = m_visibleWrappedLineInAnnotationGroup; m_lastAnnotationGroupIdentifier = m_nextAnnotationGroupIdentifier; m_isSameAnnotationGroupsSinceLast = isSameAnnotationGroupsSinceThis; } void KateIconBorder::paintBorder(int /*x*/, int y, int /*width*/, int height) { const uint h = m_view->renderer()->lineHeight(); const uint startz = (y / h); const uint endz = qMin(startz + 1 + (height / h), static_cast(m_viewInternal->cache()->viewCacheLineCount())); const uint currentLine = m_view->cursorPosition().line(); // center the folding boxes int m_px = (h - 11) / 2; if (m_px < 0) { m_px = 0; } // Ensure we miss no change of the count of line number digits const int newNeededWidth = lineNumberWidth(); if (m_updatePositionToArea || (newNeededWidth != m_lineNumberAreaWidth)) { m_lineNumberAreaWidth = newNeededWidth; m_updatePositionToArea = true; m_positionToArea.clear(); } QPainter p(this); p.setRenderHints(QPainter::TextAntialiasing); p.setFont(m_view->renderer()->config()->font()); // for line numbers KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, m_hoveredAnnotationGroupIdentifier, startz, m_annotationBorderOn); // Fetch often used data only once, improve readability const int w = width(); const QColor iconBarColor = m_view->renderer()->config()->iconBarColor(); // Effective our background const QColor lineNumberColor = m_view->renderer()->config()->lineNumberColor(); const QColor backgroundColor = m_view->renderer()->config()->backgroundColor(); // Of the edit area // Paint the border in chunks line by line for (uint z = startz; z < endz; z++) { // Painting coordinates, lineHeight * lineNumber const uint y = h * z; // Paint the border in chunks left->right, remember used width uint lnX = 0; // Paint background over full width... p.fillRect(lnX, y, w, h, iconBarColor); // ...and overpaint again the end to simulate some margin to the edit area, // so that the text not looks like stuck to the border p.fillRect(w - m_separatorWidth, y, w, h, backgroundColor); const KateTextLayout lineLayout = m_viewInternal->cache()->viewLine(z); int realLine = lineLayout.line(); if (realLine < 0) { // We have reached the end of the document, just paint background continue; } // icon pane if (m_iconBorderOn) { p.setPen(m_view->renderer()->config()->separatorColor()); p.setBrush(m_view->renderer()->config()->separatorColor()); p.drawLine(lnX + m_iconAreaWidth, y, lnX + m_iconAreaWidth, y + h); const uint mrk(m_doc->mark(realLine)); // call only once if (mrk && lineLayout.startCol() == 0) { for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { QPixmap px_mark(m_doc->markPixmap(markType)); px_mark.setDevicePixelRatio(devicePixelRatioF()); if (!px_mark.isNull() && h > 0 && m_iconAreaWidth > 0) { // scale up to a usable size const int s = qMin(m_iconAreaWidth * devicePixelRatioF(), h * devicePixelRatioF()) - 2; px_mark = px_mark.scaled(s, s, Qt::KeepAspectRatio, Qt::SmoothTransformation); // center the mark pixmap int x_px = 0.5 * qMax(m_iconAreaWidth - (s / devicePixelRatioF()), 0.0); int y_px = 0.5 * qMax(h - (s / devicePixelRatioF()), 0.0); p.drawPixmap(lnX + x_px, y + y_px, px_mark); } } } } lnX += m_iconAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, IconBorder)); } } // annotation information if (m_annotationBorderOn) { // Draw a border line between annotations and the line numbers p.setPen(lineNumberColor); p.setBrush(lineNumberColor); const qreal borderX = lnX + m_annotationAreaWidth + 0.5; p.drawLine(QPointF(borderX, y+0.5), QPointF(borderX, y + h - 0.5)); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect.setRect(lnX, y, m_annotationAreaWidth, h); annotationGroupPositionState.nextLine(styleOption, z, realLine); m_annotationItemDelegate->paint(&p, styleOption, model, realLine); } lnX += m_annotationAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, AnnotationBorder)); } } // line number if (m_lineNumbersOn || m_dynWrapIndicatorsOn) { QColor usedLineNumberColor; const int distanceToCurrent = abs(realLine - static_cast(currentLine)); if (distanceToCurrent == 0) { usedLineNumberColor = m_view->renderer()->config()->currentLineNumberColor(); } else { usedLineNumberColor = lineNumberColor; } p.setPen(usedLineNumberColor); p.setBrush(usedLineNumberColor); if (lineLayout.startCol() == 0) { if (m_relLineNumbersOn) { if (distanceToCurrent == 0) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter, QString::number(realLine + 1)); } else { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(distanceToCurrent)); } if (m_updateRelLineNumbers) { m_updateRelLineNumbers = false; update(); } } else if (m_lineNumbersOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(realLine + 1)); } } else if (m_dynWrapIndicatorsOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, m_dynWrapIndicatorChar); } lnX += m_lineNumberAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, LineNumbers)); } } // modified line system if (m_view->config()->lineModification() && !m_doc->url().isEmpty()) { const Kate::TextLine tl = m_doc->plainKateTextLine(realLine); if (tl->markedAsModified()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->modifiedLineColor()); } else if (tl->markedAsSavedOnDisk()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->savedLineColor()); } else { p.fillRect(lnX, y, m_modAreaWidth, h, iconBarColor); } lnX += m_modAreaWidth; // No m_separatorWidth if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, None)); } } // folding markers if (m_foldingMarkersOn) { const QColor foldingColor(m_view->renderer()->config()->foldingColor()); // possible additional folding highlighting if (m_foldingRange && m_foldingRange->overlapsLine(realLine)) { p.fillRect(lnX, y, m_foldingAreaWidth, h, foldingColor); } if (lineLayout.startCol() == 0) { QVector > startingRanges = m_view->textFolding().foldingRangesStartingOnLine(realLine); bool anyFolded = false; for (int i = 0; i < startingRanges.size(); ++i) { if (startingRanges[i].second & Kate::TextFolding::Folded) { anyFolded = true; } } const Kate::TextLine tl = m_doc->kateTextLine(realLine); if (!startingRanges.isEmpty() || tl->markedAsFoldingStart()) { if (anyFolded) { paintTriangle(p, foldingColor, lnX, y, m_foldingAreaWidth, h, false); } else { // Don't try to use currentLineNumberColor, the folded icon gets also not highligted paintTriangle(p, lineNumberColor, lnX, y, m_foldingAreaWidth, h, true); } } } lnX += m_foldingAreaWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, FoldingMarkers)); } } if (m_updatePositionToArea) { m_updatePositionToArea = false; // Don't forget our "text-stuck-to-border" protector lnX += m_separatorWidth; m_positionToArea.append(AreaPosition(lnX, None)); // Now that we know our needed space, ensure we are painted properly updateGeometry(); update(); return; } } } KateIconBorder::BorderArea KateIconBorder::positionToArea(const QPoint &p) const { for (int i = 0; i < m_positionToArea.size(); ++i) { if (p.x() <= m_positionToArea.at(i).first) { return m_positionToArea.at(i).second; } } return None; } void KateIconBorder::mousePressEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (t.isValid()) { m_lastClickedLine = t.line(); const auto area = positionToArea(e->pos()); // IconBorder and AnnotationBorder have their own behavior; don't forward to view if (area != IconBorder && area != AnnotationBorder) { const auto pos = QPoint(0, e->y()); if (area == LineNumbers && e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) { // setup view so the following mousePressEvent will select the line m_viewInternal->beginSelectLine(pos); } QMouseEvent forward(QEvent::MouseButtonPress, pos, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mousePressEvent(&forward); } return e->accept(); } QWidget::mousePressEvent(e); } void KateIconBorder::highlightFoldingDelayed(int line) { if ((line == m_currentLine) || (line >= m_doc->buffer().lines())) { return; } m_currentLine = line; if (m_foldingRange) { // We are for a while in the folding area, no need for delay highlightFolding(); } else if (!m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.start(); } } void KateIconBorder::highlightFolding() { /** * compute to which folding range we belong * FIXME: optimize + perhaps have some better threshold or use timers! */ KTextEditor::Range newRange = KTextEditor::Range::invalid(); for (int line = m_currentLine; line >= qMax(0, m_currentLine - 1024); --line) { /** * try if we have folding range from that line, should be fast per call */ KTextEditor::Range foldingRange = m_doc->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { continue; } /** * does the range reach us? */ if (foldingRange.overlapsLine(m_currentLine)) { newRange = foldingRange; break; } } if (newRange.isValid() && m_foldingRange && *m_foldingRange == newRange) { // new range equals the old one, nothing to do. return; } // the ranges differ, delete the old, if it exists delete m_foldingRange; m_foldingRange = nullptr; // New range, new preview! delete m_foldingPreview; bool showPreview = false; if (newRange.isValid()) { // When next line is not visible we have a folded range, only then we want a preview! showPreview = !m_view->textFolding().isLineVisible(newRange.start().line() + 1); //qCDebug(LOG_KTE) << "new folding hl-range:" << newRange; m_foldingRange = m_doc->newMovingRange(newRange, KTextEditor::MovingRange::ExpandRight); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); /** * create highlighting color * we avoid alpha as overpainting leads to ugly lines (https://bugreports.qt.io/browse/QTBUG-66036) */ attr->setBackground(QBrush(m_view->renderer()->config()->foldingColor())); m_foldingRange->setView(m_view); // use z depth defined in moving ranges interface m_foldingRange->setZDepth(-100.0); m_foldingRange->setAttribute(attr); } // show text preview, if a folded region starts here... // ...but only when main window is active (#392396) const bool isWindowActive = !window() || window()->isActiveWindow(); if (showPreview && m_view->config()->foldingPreview() && isWindowActive) { m_foldingPreview = new KateTextPreview(m_view, this); m_foldingPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_foldingPreview->setFrameStyle(QFrame::StyledPanel); // Calc how many lines can be displayed in the popup const int lineHeight = m_view->renderer()->lineHeight(); const int foldingStartLine = m_foldingRange->start().line(); // FIXME Is there really no easier way to find lineInDisplay? const QPoint pos = m_viewInternal->mapFrom(m_view, m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))); const int lineInDisplay = pos.y() / lineHeight; // Allow slightly overpainting of the view bottom to proper cover all lines const int extra = (m_viewInternal->height() % lineHeight) > (lineHeight * 0.6) ? 1 : 0; const int lineCount = qMin(m_foldingRange->numberOfLines() + 1, m_viewInternal->linesDisplayed() - lineInDisplay + extra); m_foldingPreview->resize(m_viewInternal->width(), lineCount * lineHeight + 2 * m_foldingPreview->frameWidth()); const int xGlobal = mapToGlobal(QPoint(width(), 0)).x(); const int yGlobal = m_view->mapToGlobal(m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))).y(); m_foldingPreview->move(QPoint(xGlobal, yGlobal) - m_foldingPreview->contentsRect().topLeft()); m_foldingPreview->setLine(foldingStartLine); m_foldingPreview->setCenterView(false); m_foldingPreview->setShowFoldedLines(true); m_foldingPreview->raise(); m_foldingPreview->show(); } } void KateIconBorder::hideFolding() { if (m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.stop(); } m_currentLine = -1; delete m_foldingRange; m_foldingRange = nullptr; delete m_foldingPreview; } void KateIconBorder::leaveEvent(QEvent *event) { hideFolding(); removeAnnotationHovering(); QWidget::leaveEvent(event); } void KateIconBorder::mouseMoveEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (!t.isValid()) { // Cleanup everything which may be shown removeAnnotationHovering(); hideFolding(); } else { const BorderArea area = positionToArea(e->pos()); if (area == FoldingMarkers) { highlightFoldingDelayed(t.line()); } else { hideFolding(); } if (area == AnnotationBorder) { KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { m_hoveredAnnotationGroupIdentifier = model->data( t.line(), (Qt::ItemDataRole) KTextEditor::AnnotationModel::GroupIdentifierRole ).toString(); const QPoint viewRelativePos = m_view->mapFromGlobal(e->globalPos()); QHelpEvent helpEvent(QEvent::ToolTip, viewRelativePos, e->globalPos()); KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect = annotationLineRectInView(t.line()); setStyleOptionLineData(&styleOption, e->y(), t.line(), model, m_hoveredAnnotationGroupIdentifier); m_annotationItemDelegate->helpEvent(&helpEvent, m_view, styleOption, model, t.line()); QTimer::singleShot(0, this, SLOT(update())); } } else { if (area == IconBorder) { m_doc->requestMarkTooltip(t.line(), e->globalPos()); } m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } if (area != IconBorder) { QPoint p = m_viewInternal->mapFromGlobal(e->globalPos()); QMouseEvent forward(QEvent::MouseMove, p, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseMoveEvent(&forward); } } QWidget::mouseMoveEvent(e); } void KateIconBorder::mouseReleaseEvent(QMouseEvent *e) { const int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine >= 0 && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); if (area == IconBorder) { if (e->button() == Qt::LeftButton) { if (!m_doc->handleMarkClick(cursorOnLine)) { KateViewConfig *config = m_view->config(); const uint editBits = m_doc->editableMarks(); // is the default or the only editable mark const uint singleMark = qPopulationCount(editBits) > 1 ? editBits & config->defaultMarkType() : editBits; if (singleMark) { if (m_doc->mark(cursorOnLine) & singleMark) { m_doc->removeMark(cursorOnLine, singleMark); } else { m_doc->addMark(cursorOnLine, singleMark); } } else if (config->allowMarkMenu()) { showMarkMenu(cursorOnLine, QCursor::pos()); } } } else if (e->button() == Qt::RightButton) { showMarkMenu(cursorOnLine, QCursor::pos()); } } if (area == FoldingMarkers) { // Prefer the highlighted range over the exact clicked line const int lineToToggle = m_foldingRange ? m_foldingRange->toRange().start().line() : cursorOnLine; if (e->button() == Qt::LeftButton) { m_view->toggleFoldingOfLine(lineToToggle); } else if (e->button() == Qt::RightButton) { m_view->toggleFoldingsInRange(lineToToggle); } delete m_foldingPreview; } if (area == AnnotationBorder) { const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (e->button() == Qt::LeftButton && singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } else if (e->button() == Qt::RightButton) { showAnnotationMenu(cursorOnLine, e->globalPos()); } } } QMouseEvent forward(QEvent::MouseButtonRelease, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseReleaseEvent(&forward); } void KateIconBorder::mouseDoubleClickEvent(QMouseEvent *e) { int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (area == AnnotationBorder && !singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } } QMouseEvent forward(QEvent::MouseButtonDblClick, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseDoubleClickEvent(&forward); } void KateIconBorder::wheelEvent(QWheelEvent *e) { QCoreApplication::sendEvent(m_viewInternal, e); } void KateIconBorder::showMarkMenu(uint line, const QPoint &pos) { if (m_doc->handleMarkContextMenu(line, pos)) { return; } if (!m_view->config()->allowMarkMenu()) { return; } QMenu markMenu; QMenu selectDefaultMark; auto selectDefaultMarkActionGroup = new QActionGroup(&selectDefaultMark); QVector vec(33); int i = 1; for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (!(m_doc->editableMarks() & markType)) { continue; } QAction *mA; QAction *dMA; const QPixmap icon = m_doc->markPixmap(markType); if (!m_doc->markDescription(markType).isEmpty()) { mA = markMenu.addAction(icon, m_doc->markDescription(markType)); dMA = selectDefaultMark.addAction(icon, m_doc->markDescription(markType)); } else { mA = markMenu.addAction(icon, i18n("Mark Type %1", bit + 1)); dMA = selectDefaultMark.addAction(icon, i18n("Mark Type %1", bit + 1)); } selectDefaultMarkActionGroup->addAction(dMA); mA->setData(i); mA->setCheckable(true); dMA->setData(i + 100); dMA->setCheckable(true); if (m_doc->mark(line) & markType) { mA->setChecked(true); } if (markType & KateViewConfig::global()->defaultMarkType()) { dMA->setChecked(true); } vec[i++] = markType; } if (markMenu.actions().count() == 0) { return; } if (markMenu.actions().count() > 1) { markMenu.addAction(i18n("Set Default Mark Type"))->setMenu(&selectDefaultMark); } QAction *rA = markMenu.exec(pos); if (!rA) { return; } int result = rA->data().toInt(); if (result > 100) { KateViewConfig::global()->setValue(KateViewConfig::DefaultMarkType, vec[result - 100]); } else { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes) vec[result]; if (m_doc->mark(line) & markType) { m_doc->removeMark(line, markType); } else { m_doc->addMark(line, markType); } } } KTextEditor::AbstractAnnotationItemDelegate* KateIconBorder::annotationItemDelegate() const { return m_annotationItemDelegate; } void KateIconBorder::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate) { if (delegate == m_annotationItemDelegate) { return; } // reset to default, but already on that? if (!delegate && m_isDefaultAnnotationItemDelegate) { // nothing to do return; } // make sure the tooltip is hidden if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } disconnect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (!m_isDefaultAnnotationItemDelegate) { disconnect(m_annotationItemDelegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } if (!delegate) { // reset to a default delegate m_annotationItemDelegate = new KateAnnotationItemDelegate(m_viewInternal, this); m_isDefaultAnnotationItemDelegate = true; } else { // drop any default delegate if (m_isDefaultAnnotationItemDelegate) { delete m_annotationItemDelegate; m_isDefaultAnnotationItemDelegate = false; } m_annotationItemDelegate = delegate; // catch delegate being destroyed connect(delegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (m_annotationBorderOn) { updateGeometry(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::handleDestroyedAnnotationItemDelegate() { setAnnotationItemDelegate(nullptr); } void KateIconBorder::initStyleOption(KTextEditor::StyleOptionAnnotationItem* styleOption) const { styleOption->initFrom(this); styleOption->view = m_view; styleOption->decorationSize = QSize(m_iconAreaWidth, m_iconAreaWidth); styleOption->contentFontMetrics = m_view->renderer()->config()->fontMetrics(); } void KateIconBorder::setStyleOptionLineData(KTextEditor::StyleOptionAnnotationItem* styleOption, int y, int realLine, const KTextEditor::AnnotationModel *model, const QString &annotationGroupIdentifier) const { // calculate rendered displayed line const uint h = m_view->renderer()->lineHeight(); const uint z = (y / h); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, annotationGroupIdentifier, z, true); annotationGroupPositionState.nextLine(*styleOption, z, realLine); } QRect KateIconBorder::annotationLineRectInView(int line) const { int x = 0; if (m_iconBorderOn) { x += m_iconAreaWidth + 2; } const int y = m_view->m_viewInternal->lineToY(line); return QRect(x, y, m_annotationAreaWidth, m_view->renderer()->lineHeight()); } void KateIconBorder::updateAnnotationLine(int line) { // TODO: why has the default value been 8, where is that magic number from? int width = 8; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); width = m_annotationItemDelegate->sizeHint(styleOption, model, line).width(); } if (width > m_annotationAreaWidth) { m_annotationAreaWidth = width; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::showAnnotationMenu(int line, const QPoint &pos) { QMenu menu; QAction a(i18n("Disable Annotation Bar"), &menu); a.setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); menu.addAction(&a); emit m_view->annotationContextMenuAboutToShow(m_view, &menu, line); if (menu.exec(pos) == &a) { m_view->setAnnotationBorderVisible(false); } } void KateIconBorder::hideAnnotationTooltip() { m_annotationItemDelegate->hideTooltip(m_view); } void KateIconBorder::updateAnnotationBorderWidth() { calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::calcAnnotationBorderWidth() { // TODO: another magic number, not matching the one in updateAnnotationLine() m_annotationAreaWidth = 6; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); const int lineCount = m_view->doc()->lines(); if (lineCount > 0) { const int checkedLineCount = m_hasUniformAnnotationItemSizes ? 1 : lineCount; for (int i = 0; i < checkedLineCount; ++i) { const int curwidth = m_annotationItemDelegate->sizeHint(styleOption, model, i).width(); if (curwidth > m_annotationAreaWidth) { m_annotationAreaWidth = curwidth; } } } } } void KateIconBorder::annotationModelChanged(KTextEditor::AnnotationModel *oldmodel, KTextEditor::AnnotationModel *newmodel) { if (oldmodel) { oldmodel->disconnect(this); } if (newmodel) { connect(newmodel, SIGNAL(reset()), this, SLOT(updateAnnotationBorderWidth())); connect(newmodel, SIGNAL(lineChanged(int)), this, SLOT(updateAnnotationLine(int))); } updateAnnotationBorderWidth(); } void KateIconBorder::displayRangeChanged() { hideFolding(); removeAnnotationHovering(); } //END KateIconBorder //BEGIN KateViewEncodingAction // According to http://www.iana.org/assignments/ianacharset-mib // the default/unknown mib value is 2. #define MIB_DEFAULT 2 bool lessThanAction(KSelectAction *a, KSelectAction *b) { return a->text() < b->text(); } void KateViewEncodingAction::Private::init() { QList actions; q->setToolBarMode(MenuMode); int i; foreach (const QStringList &encodingsForScript, KCharsets::charsets()->encodingsByScript()) { KSelectAction *tmp = new KSelectAction(encodingsForScript.at(0), q); for (i = 1; i < encodingsForScript.size(); ++i) { tmp->addAction(encodingsForScript.at(i)); } q->connect(tmp, SIGNAL(triggered(QAction*)), q, SLOT(_k_subActionTriggered(QAction*))); //tmp->setCheckable(true); actions << tmp; } std::sort(actions.begin(), actions.end(), lessThanAction); foreach (KSelectAction *action, actions) { q->addAction(action); } } void KateViewEncodingAction::Private::_k_subActionTriggered(QAction *action) { if (currentSubAction == action) { return; } currentSubAction = action; bool ok = false; int mib = q->mibForName(action->text(), &ok); if (ok) { emit q->KSelectAction::triggered(action->text()); emit q->triggered(q->codecForMib(mib)); } } KateViewEncodingAction::KateViewEncodingAction(KTextEditor::DocumentPrivate *_doc, KTextEditor::ViewPrivate *_view, const QString &text, QObject *parent, bool saveAsMode) : KSelectAction(text, parent), doc(_doc), view(_view), d(new Private(this)) , m_saveAsMode(saveAsMode) { d->init(); connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); } KateViewEncodingAction::~KateViewEncodingAction() { delete d; } void KateViewEncodingAction::slotAboutToShow() { setCurrentCodec(doc->config()->encoding()); } void KateViewEncodingAction::setEncoding(const QString &e) { /** * in save as mode => trigger saveAs */ if (m_saveAsMode) { doc->documentSaveAsWithEncoding(e); return; } /** * else switch encoding */ doc->userSetEncodingForNextReload(); doc->setEncoding(e); view->reloadFile(); } int KateViewEncodingAction::mibForName(const QString &codecName, bool *ok) const { // FIXME logic is good but code is ugly bool success = false; int mib = MIB_DEFAULT; KCharsets *charsets = KCharsets::charsets(); QTextCodec *codec = charsets->codecForName(codecName, success); if (!success) { // Maybe we got a description name instead codec = charsets->codecForName(charsets->encodingForName(codecName), success); } if (codec) { mib = codec->mibEnum(); } if (ok) { *ok = success; } if (success) { return mib; } qCWarning(LOG_KTE) << "Invalid codec name: " << codecName; return MIB_DEFAULT; } QTextCodec *KateViewEncodingAction::codecForMib(int mib) const { if (mib == MIB_DEFAULT) { // FIXME offer to change the default codec return QTextCodec::codecForLocale(); } else { return QTextCodec::codecForMib(mib); } } QTextCodec *KateViewEncodingAction::currentCodec() const { return codecForMib(currentCodecMib()); } bool KateViewEncodingAction::setCurrentCodec(QTextCodec *codec) { disconnect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); int i, j; for (i = 0; i < actions().size(); ++i) { if (actions().at(i)->menu()) { for (j = 0; j < actions().at(i)->menu()->actions().size(); ++j) { if (!j && !actions().at(i)->menu()->actions().at(j)->data().isNull()) { continue; } if (actions().at(i)->menu()->actions().at(j)->isSeparator()) { continue; } if (codec == KCharsets::charsets()->codecForName(actions().at(i)->menu()->actions().at(j)->text())) { d->currentSubAction = actions().at(i)->menu()->actions().at(j); d->currentSubAction->setChecked(true); } else { actions().at(i)->menu()->actions().at(j)->setChecked(false); } } } } connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); return true; } QString KateViewEncodingAction::currentCodecName() const { return d->currentSubAction->text(); } bool KateViewEncodingAction::setCurrentCodec(const QString &codecName) { return setCurrentCodec(KCharsets::charsets()->codecForName(codecName)); } int KateViewEncodingAction::currentCodecMib() const { return mibForName(currentCodecName()); } bool KateViewEncodingAction::setCurrentCodec(int mib) { return setCurrentCodec(codecForMib(mib)); } //END KateViewEncodingAction //BEGIN KateViewBar related classes KateViewBarWidget::KateViewBarWidget(bool addCloseButton, QWidget *parent) : QWidget(parent) , m_viewBar(nullptr) { QHBoxLayout *layout = new QHBoxLayout(this); // NOTE: Here be cosmetics. layout->setContentsMargins(0, 0, 0, 0); // hide button if (addCloseButton) { QToolButton *hideButton = new QToolButton(this); hideButton->setAutoRaise(true); hideButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); connect(hideButton, SIGNAL(clicked()), SIGNAL(hideMe())); layout->addWidget(hideButton); layout->setAlignment(hideButton, Qt::AlignLeft | Qt::AlignTop); } // widget to be used as parent for the real content m_centralWidget = new QWidget(this); layout->addWidget(m_centralWidget); setLayout(layout); setFocusProxy(m_centralWidget); } KateViewBar::KateViewBar(bool external, QWidget *parent, KTextEditor::ViewPrivate *view) : QWidget(parent), m_external(external), m_view(view), m_permanentBarWidget(nullptr) { m_layout = new QVBoxLayout(this); m_stack = new QStackedWidget(this); m_layout->addWidget(m_stack); m_layout->setContentsMargins(0, 0, 0, 0); m_stack->hide(); hide(); } void KateViewBar::addBarWidget(KateViewBarWidget *newBarWidget) { // just ignore additional adds for already existing widgets if (hasBarWidget(newBarWidget)) { return; } // add new widget, invisible... newBarWidget->hide(); m_stack->addWidget(newBarWidget); newBarWidget->setAssociatedViewBar(this); connect(newBarWidget, SIGNAL(hideMe()), SLOT(hideCurrentBarWidget())); } void KateViewBar::removeBarWidget(KateViewBarWidget *barWidget) { // remove only if there if (!hasBarWidget(barWidget)) { return; } m_stack->removeWidget(barWidget); barWidget->setAssociatedViewBar(nullptr); barWidget->hide(); disconnect(barWidget, nullptr, this, nullptr); } void KateViewBar::addPermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget); Q_ASSERT(!m_permanentBarWidget); m_stack->addWidget(barWidget); m_stack->setCurrentWidget(barWidget); m_stack->show(); m_permanentBarWidget = barWidget; m_permanentBarWidget->show(); setViewBarVisible(true); } void KateViewBar::removePermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(m_permanentBarWidget == barWidget); Q_UNUSED(barWidget); const bool hideBar = m_stack->currentWidget() == m_permanentBarWidget; m_permanentBarWidget->hide(); m_stack->removeWidget(m_permanentBarWidget); m_permanentBarWidget = nullptr; if (hideBar) { m_stack->hide(); setViewBarVisible(false); } } bool KateViewBar::hasPermanentWidget(KateViewBarWidget *barWidget) const { return (m_permanentBarWidget == barWidget); } void KateViewBar::showBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget != nullptr); if (barWidget != qobject_cast(m_stack->currentWidget())) { hideCurrentBarWidget(); } // raise correct widget m_stack->setCurrentWidget(barWidget); barWidget->show(); barWidget->setFocus(Qt::ShortcutFocusReason); m_stack->show(); setViewBarVisible(true); } bool KateViewBar::hasBarWidget(KateViewBarWidget *barWidget) const { return m_stack->indexOf(barWidget) != -1; } void KateViewBar::hideCurrentBarWidget() { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (current) { current->closed(); } // if we have any permanent widget, make it visible again if (m_permanentBarWidget) { m_stack->setCurrentWidget (m_permanentBarWidget); } else { // else: hide the bar m_stack->hide(); setViewBarVisible(false); } m_view->setFocus(); } void KateViewBar::setViewBarVisible(bool visible) { if (m_external) { if (visible) { m_view->mainWindow()->showViewBar(m_view); } else { m_view->mainWindow()->hideViewBar(m_view); } } else { setVisible(visible); } } bool KateViewBar::hiddenOrPermanent() const { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (!isVisible() || (m_permanentBarWidget && m_permanentBarWidget == current)) { return true; } return false; } void KateViewBar::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { hideCurrentBarWidget(); return; } QWidget::keyPressEvent(event); } void KateViewBar::hideEvent(QHideEvent *event) { Q_UNUSED(event); // if (!event->spontaneous()) // m_view->setFocus(); } //END KateViewBar related classes KatePasteMenu::KatePasteMenu(const QString &text, KTextEditor::ViewPrivate *view) : KActionMenu(text, view) , m_view(view) { connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); } void KatePasteMenu::slotAboutToShow() { menu()->clear(); /** * insert complete paste history */ int i = 0; Q_FOREACH (const QString &text, KTextEditor::EditorPrivate::self()->clipboardHistory()) { /** * get text for the menu ;) */ QString leftPart = (text.size() > 48) ? (text.left(48) + QLatin1String("...")) : text; QAction *a = menu()->addAction(leftPart.replace(QLatin1Char('\n'), QLatin1Char(' ')), this, SLOT(paste())); a->setData(i++); } } void KatePasteMenu::paste() { if (!sender()) { return; } QAction *action = qobject_cast(sender()); if (!action) { return; } // get index int i = action->data().toInt(); if (i >= KTextEditor::EditorPrivate::self()->clipboardHistory().size()) { return; } // paste m_view->paste(&KTextEditor::EditorPrivate::self()->clipboardHistory()[i]); } diff --git a/src/vimode/appcommands.cpp b/src/vimode/appcommands.cpp index 0252fa04..a94b332e 100644 --- a/src/vimode/appcommands.cpp +++ b/src/vimode/appcommands.cpp @@ -1,525 +1,525 @@ /* This file is part of the KDE libraries Copyright (C) 2009 Erlend Hamberg Copyright (C) 2011 Svyatoslav Kuzmich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include using namespace KateVi; //BEGIN AppCommands AppCommands *AppCommands::m_instance = nullptr; AppCommands::AppCommands() : KTextEditor::Command({ QStringLiteral("q"), QStringLiteral("qa"), QStringLiteral("qall"), QStringLiteral("q!"), QStringLiteral("qa!"), QStringLiteral("qall!") , QStringLiteral("w"), QStringLiteral("wq"), QStringLiteral("wa"), QStringLiteral("wqa"), QStringLiteral("x"), QStringLiteral("xa"), QStringLiteral("new") , QStringLiteral("vnew"), QStringLiteral("e"), QStringLiteral("edit"), QStringLiteral("enew"), QStringLiteral("sp"), QStringLiteral("split"), QStringLiteral("vs") , QStringLiteral("vsplit"), QStringLiteral("only"), QStringLiteral("tabe"), QStringLiteral("tabedit"), QStringLiteral("tabnew"), QStringLiteral("bd") , QStringLiteral("bdelete"), QStringLiteral("tabc"), QStringLiteral("tabclose"), QStringLiteral("clo"), QStringLiteral("close") }) , re_write(QStringLiteral("^w(a)?$")) , re_close(QStringLiteral("^bd(elete)?|tabc(lose)?$")) , re_quit(QStringLiteral("^(w)?q(a|all)?(!)?$")) , re_exit(QStringLiteral("^x(a)?$")) , re_edit(QStringLiteral("^e(dit)?|tabe(dit)?|tabnew$")) , re_tabedit(QStringLiteral("^tabe(dit)?|tabnew$")) , re_new(QStringLiteral("^(v)?new$")) , re_split(QStringLiteral("^sp(lit)?$")) , re_vsplit(QStringLiteral("^vs(plit)?$")) , re_vclose(QStringLiteral("^clo(se)?$")) , re_only(QStringLiteral("^on(ly)?$")) { } AppCommands::~AppCommands() { m_instance = nullptr; } bool AppCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &) { - QStringList args(cmd.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts)) ; + QStringList args(cmd.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts)) ; QString command(args.takeFirst()); KTextEditor::MainWindow *mainWin = view->mainWindow(); KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); QRegularExpressionMatch match; if ((match = re_write.match(command)).hasMatch()) { //TODO: handle writing to specific file if (!match.captured(1).isEmpty()) { // [a]ll Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } msg = i18n("All documents written to disk"); } else { view->document()->documentSave(); msg = i18n("Document written to disk"); } } // Other buffer commands are implemented by the KateFileTree plugin else if ((match = re_close.match(command)).hasMatch()) { QTimer::singleShot(0, [app, view](){ app->closeDocument(view->document()); }); } else if ((match = re_quit.match(command)).hasMatch()) { const bool save = !match.captured(1).isEmpty(); // :[w]q const bool allDocuments = !match.captured(2).isEmpty(); // :q[all] const bool doNotPromptForSave = !match.captured(3).isEmpty(); // :q[!] if (allDocuments) { if (save) { Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } } if (doNotPromptForSave) { Q_FOREACH(KTextEditor::Document *doc, app->documents()) { if (doc->isModified()) { doc->setModified(false); } } } QTimer::singleShot(0, this, SLOT(quit())); } else { if (save && view->document()->isModified()) { view->document()->documentSave(); } if (doNotPromptForSave) { view->document()->setModified(false); } if (mainWin->views().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentView())); } else { if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } } else if ((match = re_exit.match(command)).hasMatch()) { if (!match.captured(1).isEmpty()) { // a[ll] Q_FOREACH(KTextEditor::Document *doc, app->documents()) { doc->save(); } QTimer::singleShot(0, this, SLOT(quit())); } else { if (view->document()->isModified()) { view->document()->documentSave(); } if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } else if ((match = re_edit.match(command)).hasMatch()) { QString argument = args.join(QLatin1Char(' ')); if (argument.isEmpty() || argument == QLatin1String("!")) { if ((match = re_tabedit.match(command)).hasMatch()) { if (auto doc = app->openUrl(QUrl())) { QTimer::singleShot(0, [mainWin, doc](){ mainWin->activateView(doc); }); } } else { view->document()->documentReload(); } } else { QUrl base = view->document()->url(); QUrl url; QUrl arg2path(argument); if (base.isValid()) { // first try to use the same path as the current open document has url = QUrl(base.resolved(arg2path)); //resolved handles the case where the args is a relative path, and is the same as using QUrl(args) elsewise } else { // else use the cwd url = QUrl(QUrl(QDir::currentPath() + QLatin1Char('/')).resolved(arg2path)); // + "/" is needed because of http://lists.qt.nokia.com/public/qt-interest/2011-May/033913.html } QFileInfo file(url.toLocalFile()); KTextEditor::Document *doc = app->findUrl(url); if (!doc) { if (file.exists()) { doc = app->openUrl(url); } else { if ((doc = app->openUrl(QUrl()))) { doc->saveAs(url); } } } if (doc) { QTimer::singleShot(0, [mainWin, doc](){ mainWin->activateView(doc); }); } } // splitView() orientations are reversed from the usual editor convention. // 'vsplit' and 'vnew' use Qt::Horizontal to match vi and the Kate UI actions. } else if ((match = re_new.match(command)).hasMatch()) { if (match.captured(1) == QLatin1String("v")) { // vertical split mainWin->splitView(Qt::Horizontal); } else { // horizontal split mainWin->splitView(Qt::Vertical); } mainWin->openUrl(QUrl()); } else if (command == QLatin1String("enew")) { mainWin->openUrl(QUrl()); } else if ((match = re_split.match(command)).hasMatch()) { mainWin->splitView(Qt::Vertical); // see above } else if ((match = re_vsplit.match(command)).hasMatch()) { mainWin->splitView(Qt::Horizontal); } else if ((match = re_vclose.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeCurrentSplitView())); } else if ((match = re_only.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeOtherSplitViews())); } return true; } bool AppCommands::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view); if (re_write.match(cmd).hasMatch()) { msg = i18n("

w/wa — write document(s) to disk

" "

Usage: w[a]

" "

Writes the current document(s) to disk. " "It can be called in two ways:
" " w — writes the current document to disk
" " wa — writes all documents to disk.

" "

If no file name is associated with the document, " "a file dialog will be shown.

"); return true; } else if (re_quit.match(cmd).hasMatch()) { msg = i18n("

q/qa/wq/wqa — [write and] quit

" "

Usage: [w]q[a]

" "

Quits the application. If w is prepended, it also writes" " the document(s) to disk. This command " "can be called in several ways:
" " q — closes the current view.
" " qa — closes all views, effectively quitting the application.
" " wq — writes the current document to disk and closes its view.
" " wqa — writes all documents to disk and quits.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

"); return true; } else if (re_exit.match(cmd).hasMatch()) { msg = i18n("

x/xa — write and quit

" "

Usage: x[a]

" "

Saves document(s) and quits (exits). This command " "can be called in two ways:
" " x — closes the current view.
" " xa — closes all views, effectively quitting the application.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

" "

Unlike the 'w' commands, this command only writes the document if it is modified." "

"); return true; } else if (re_split.match(cmd).hasMatch()) { msg = i18n("

sp,split— Split horizontally the current view into two

" "

Usage: sp[lit]

" "

The result is two views on the same document.

"); return true; } else if (re_vsplit.match(cmd).hasMatch()) { msg = i18n("

vs,vsplit— Split vertically the current view into two

" "

Usage: vs[plit]

" "

The result is two views on the same document.

"); return true; } else if (re_vclose.match(cmd).hasMatch()) { msg = i18n("

clo[se]— Close the current view

" "

Usage: clo[se]

" "

After executing it, the current view will be closed.

"); return true; } else if (re_new.match(cmd).hasMatch()) { msg = i18n("

[v]new — split view and create new document

" "

Usage: [v]new

" "

Splits the current view and opens a new document in the new view." " This command can be called in two ways:
" " new — splits the view horizontally and opens a new document.
" " vnew — splits the view vertically and opens a new document.
" "

"); return true; } else if (re_edit.match(cmd).hasMatch()) { msg = i18n("

e[dit] — reload current document

" "

Usage: e[dit]

" "

Starts editing the current document again. This is useful to re-edit" " the current file, when it has been changed by another program.

"); return true; } return false; } KTextEditor::View * AppCommands::findViewInDifferentSplitView(KTextEditor::MainWindow *window, KTextEditor::View *view) { Q_FOREACH (KTextEditor::View *it, window->views()) { if (!window->viewsInSameSplitView(it, view)) { return it; } } return nullptr; } void AppCommands::closeCurrentDocument() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::Document *doc = app->activeMainWindow()->activeView()->document(); QTimer::singleShot(0, [app, doc](){ app->closeDocument(doc); }); } void AppCommands::closeCurrentView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeView(mw->activeView()); } void AppCommands::closeCurrentSplitView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeSplitView(mw->activeView()); } void AppCommands::closeOtherSplitViews() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); KTextEditor::View *view = mw->activeView(); KTextEditor::View *viewToRemove = nullptr; while ((viewToRemove = findViewInDifferentSplitView(mw, view))) { mw->closeSplitView(viewToRemove); } } void AppCommands::quit() { KTextEditor::Editor::instance()->application()->quit(); } //END AppCommands //BEGIN KateViBufferCommand BufferCommands *BufferCommands::m_instance = nullptr; BufferCommands::BufferCommands() : KTextEditor::Command({ QStringLiteral("ls") , QStringLiteral("b"), QStringLiteral("buffer") , QStringLiteral("bn"), QStringLiteral("bnext"), QStringLiteral("bp"), QStringLiteral("bprevious") , QStringLiteral("tabn"), QStringLiteral("tabnext"), QStringLiteral("tabp"), QStringLiteral("tabprevious") , QStringLiteral("bf"), QStringLiteral("bfirst"), QStringLiteral("bl"), QStringLiteral("blast") , QStringLiteral("tabf"), QStringLiteral("tabfirst"), QStringLiteral("tabl"), QStringLiteral("tablast")}) { } BufferCommands::~BufferCommands() { m_instance = nullptr; } bool BufferCommands::exec(KTextEditor::View *view, const QString &cmd, QString &, const KTextEditor::Range &) { // create list of args QStringList args(cmd.split(QLatin1Char(' '), QString::KeepEmptyParts)); QString command = args.takeFirst(); // same as cmd if split failed QString argument = args.join(QLatin1Char(' ')); if (command == QLatin1String("ls")) { // TODO: open quickview } else if (command == QLatin1String("b") || command == QLatin1String("buffer")) { switchDocument(view, argument); } else if (command == QLatin1String("bp") || command == QLatin1String("bprevious")) { prevBuffer(view); } else if (command == QLatin1String("bn") || command == QLatin1String("bnext")) { nextBuffer(view); } else if (command == QLatin1String("bf") || command == QLatin1String("bfirst")) { firstBuffer(view); } else if (command == QLatin1String("bl") || command == QLatin1String("blast")) { lastBuffer(view); } else if (command == QLatin1String("tabn") || command == QLatin1String("tabnext")) { nextTab(view); } else if (command == QLatin1String("tabp") || command == QLatin1String("tabprevious")) { prevTab(view); } else if (command == QLatin1String("tabf") || command == QLatin1String("tabfirst")) { firstTab(view); } else if (command == QLatin1String("tabl") || command == QLatin1String("tablast")) { lastTab(view); } return true; } void BufferCommands::switchDocument(KTextEditor::View *view, const QString &address) { if (address.isEmpty()) { // no argument: switch to the previous document prevBuffer(view); return; } const int idx = address.toInt(); QList docs = documents(); if (idx > 0 && idx <= docs.size()) { // numerical argument: switch to the nth document activateDocument(view, docs.at(idx - 1)); } else { // string argument: switch to the given file KTextEditor::Document *doc = nullptr; Q_FOREACH(KTextEditor::Document *it, docs) { if (it->documentName() == address) { doc = it; break; } } if (doc) { activateDocument(view, doc); } } } void BufferCommands::prevBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx > 0) { activateDocument(view, docs.at(idx - 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.last()); } } void BufferCommands::nextBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx + 1 < docs.size()) { activateDocument(view, docs.at(idx + 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.first()); } } void BufferCommands::firstBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().at(0)); } } void BufferCommands::lastBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().last()); } } void BufferCommands::prevTab(KTextEditor::View *view) { prevBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::nextTab(KTextEditor::View *view) { nextBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::firstTab(KTextEditor::View *view) { firstBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::lastTab(KTextEditor::View *view) { lastBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::activateDocument(KTextEditor::View *view, KTextEditor::Document *doc) { KTextEditor::MainWindow *mainWindow = view->mainWindow(); QTimer::singleShot(0, [mainWindow, doc]() { mainWindow->activateView(doc); }); } QList< KTextEditor::Document * > BufferCommands::documents() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); return app->documents(); } bool BufferCommands::help(KTextEditor::View * /*view*/, const QString &cmd, QString &msg) { if (cmd == QLatin1String("b") || cmd == QLatin1String("buffer")) { msg = i18n("

b,buffer — Edit document N from the document list

" "

Usage: b[uffer] [N]

"); return true; } else if (cmd == QLatin1String("bp") || cmd == QLatin1String("bprevious") || cmd == QLatin1String("tabp") || cmd == QLatin1String("tabprevious")) { msg = i18n("

bp,bprev — previous buffer

" "

Usage: bp[revious] [N]

" "

Goes to [N]th previous document (\"buffer\") in document list.

" "

[N] defaults to one.

" "

Wraps around the start of the document list.

"); return true; } else if (cmd == QLatin1String("bn") || cmd == QLatin1String("bnext") || cmd == QLatin1String("tabn") || cmd == QLatin1String("tabnext")) { msg = i18n("

bn,bnext — switch to next document

" "

Usage: bn[ext] [N]

" "

Goes to [N]th next document (\"buffer\") in document list." "[N] defaults to one.

" "

Wraps around the end of the document list.

"); return true; } else if (cmd == QLatin1String("bf") || cmd == QLatin1String("bfirst") || cmd == QLatin1String("tabf") || cmd == QLatin1String("tabfirst")) { msg = i18n("

bf,bfirst — first document

" "

Usage: bf[irst]

" "

Goes to the first document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("bl") || cmd == QLatin1String("blast") || cmd == QLatin1String("tabl") || cmd == QLatin1String("tablast")) { msg = i18n("

bl,blast — last document

" "

Usage: bl[ast]

" "

Goes to the last document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("ls")) { msg = i18n("

ls

" "

list current buffers

"); } return false; } //END KateViBufferCommand diff --git a/src/vimode/cmds.cpp b/src/vimode/cmds.cpp index 17dfaa59..fe4f0d28 100644 --- a/src/vimode/cmds.cpp +++ b/src/vimode/cmds.cpp @@ -1,280 +1,280 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2003-2005 Anders Lund * Copyright (C) 2001-2010 Christoph Cullmann * Copyright (C) 2001 Charles Samuels * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include "katedocument.h" #include "kateview.h" #include "kateglobal.h" #include #include #include "katecmd.h" #include "katepartdebug.h" #include "kateviinputmode.h" #include #include "globalstate.h" #include "marks.h" #include #include #include #include using namespace KateVi; //BEGIN ViCommands Commands *Commands::m_instance = nullptr; bool Commands::exec(KTextEditor::View *view, const QString &_cmd, QString &msg, const KTextEditor::Range &range) { Q_UNUSED(range) // cast it hardcore, we know that it is really a kateview :) KTextEditor::ViewPrivate *v = static_cast(view); if (!v) { msg = i18n("Could not access view"); return false; } //create a list of args - QStringList args(_cmd.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts)); + QStringList args(_cmd.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts)); QString cmd(args.takeFirst()); // ALL commands that takes no arguments. if (mappingCommands().contains(cmd)) { if (cmd.endsWith(QLatin1String("unmap"))) { if (args.count() == 1) { m_viGlobal->mappings()->remove(modeForMapCommand(cmd), args.at(0)); return true; } else { msg = i18n("Missing argument. Usage: %1 ", cmd); return false; } } if (args.count() == 1) { msg = m_viGlobal->mappings()->get(modeForMapCommand(cmd), args.at(0), true); if (msg.isEmpty()) { msg = i18n("No mapping found for \"%1\"", args.at(0)); return false; } else { msg = i18n("\"%1\" is mapped to \"%2\"", args.at(0), msg); } } else if (args.count() == 2) { Mappings::MappingRecursion mappingRecursion = (isMapCommandRecursive(cmd)) ? Mappings::Recursive : Mappings::NonRecursive; m_viGlobal->mappings()->add(modeForMapCommand(cmd), args.at(0), args.at(1), mappingRecursion); } else { msg = i18n("Missing argument(s). Usage: %1 []", cmd); return false; } return true; } NormalViMode *nm = m_viInputModeManager->getViNormalMode(); if (cmd == QLatin1String("d") || cmd == QLatin1String("delete") || cmd == QLatin1String("j") || cmd == QLatin1String("c") || cmd == QLatin1String("change") || cmd == QLatin1String("<") || cmd == QLatin1String(">") || cmd == QLatin1String("y") || cmd == QLatin1String("yank")) { KTextEditor::Cursor start_cursor_position = v->cursorPosition(); int count = 1; if (range.isValid()) { count = qAbs(range.end().line() - range.start().line()) + 1; v->setCursorPosition(KTextEditor::Cursor(qMin(range.start().line(), range.end().line()), 0)); } - static const QRegularExpression number(QLatin1String("^(\\d+)$")); + static const QRegularExpression number(QStringLiteral("^(\\d+)$")); for (int i = 0; i < args.count(); i++) { auto match = number.match(args.at(i)); if (match.hasMatch()) { count += match.captured(0).toInt() - 1; } QChar r = args.at(i).at(0); if (args.at(i).size() == 1 && ((r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*'))) { nm->setRegister(r); } } nm->setCount(count); if (cmd == QLatin1String("d") || cmd == QLatin1String("delete")) { nm->commandDeleteLine(); } if (cmd == QLatin1String("j")) { nm->commandJoinLines(); } if (cmd == QLatin1String("c") || cmd == QLatin1String("change")) { nm->commandChangeLine(); } if (cmd == QLatin1String("<")) { nm->commandUnindentLine(); } if (cmd == QLatin1String(">")) { nm->commandIndentLine(); } if (cmd == QLatin1String("y") || cmd == QLatin1String("yank")) { nm->commandYankLine(); v->setCursorPosition(start_cursor_position); } // TODO - should we resetParser, here? We'd have to make it public, if so. // Or maybe synthesise a KateViCommand to execute instead ... ? nm->setCount(0); return true; } if (cmd == QLatin1String("mark") || cmd == QLatin1String("ma") || cmd == QLatin1String("k")) { if (args.count() == 0) { if (cmd == QLatin1String("mark")) { // TODO: show up mark list; } else { msg = i18n("Wrong arguments"); return false; } } else if (args.count() == 1) { QChar r = args.at(0).at(0); int line; if ((r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*')) { if (range.isValid()) { line = qMax(range.end().line(), range.start().line()); } else { line = v->cursorPosition().line(); } m_viInputModeManager->marks()->setUserMark(r, KTextEditor::Cursor(line, 0)); } } else { msg = i18n("Wrong arguments"); return false; } return true; } // should not happen :) msg = i18n("Unknown command '%1'", cmd); return false; } bool Commands::supportsRange(const QString &range) { static QStringList l; if (l.isEmpty()) l << QStringLiteral("d") << QStringLiteral("delete") << QStringLiteral("j") << QStringLiteral("c") << QStringLiteral("change") << QStringLiteral("<") << QStringLiteral(">") << QStringLiteral("y") << QStringLiteral("yank") << QStringLiteral("ma") << QStringLiteral("mark") << QStringLiteral("k"); return l.contains(range.split(QLatin1Char(' ')).at(0)); } KCompletion *Commands::completionObject(KTextEditor::View *view, const QString &cmd) { Q_UNUSED(view) KTextEditor::ViewPrivate *v = static_cast(view); if (v && (cmd == QLatin1String("nn") || cmd == QLatin1String("nnoremap"))) { QStringList l = m_viGlobal->mappings()->getAll(Mappings::NormalModeMapping); KateCmdShellCompletion *co = new KateCmdShellCompletion(); co->setItems(l); co->setIgnoreCase(false); return co; } return nullptr; } const QStringList &Commands::mappingCommands() { static QStringList mappingsCommands; if (mappingsCommands.isEmpty()) { mappingsCommands << QStringLiteral("nmap") << QStringLiteral("nm") << QStringLiteral("noremap") << QStringLiteral("nnoremap") << QStringLiteral("nn") << QStringLiteral("no") << QStringLiteral("vmap") << QStringLiteral("vm") << QStringLiteral("vnoremap") << QStringLiteral("vn") << QStringLiteral("imap") << QStringLiteral("im") << QStringLiteral("inoremap") << QStringLiteral("ino") << QStringLiteral("cmap") << QStringLiteral("cm") << QStringLiteral("cnoremap") << QStringLiteral("cno"); mappingsCommands << QStringLiteral("nunmap") << QStringLiteral("vunmap") << QStringLiteral("iunmap") << QStringLiteral("cunmap"); } return mappingsCommands; } Mappings::MappingMode Commands::modeForMapCommand(const QString &mapCommand) { static QMap modeForMapCommand; if (modeForMapCommand.isEmpty()) { // Normal is the default. modeForMapCommand.insert(QStringLiteral("vmap"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("vm"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("vnoremap"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("vn"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("imap"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("im"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("inoremap"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("ino"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("cmap"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("cm"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("cnoremap"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("cno"), Mappings::CommandModeMapping); modeForMapCommand.insert(QStringLiteral("nunmap"), Mappings::NormalModeMapping); modeForMapCommand.insert(QStringLiteral("vunmap"), Mappings::VisualModeMapping); modeForMapCommand.insert(QStringLiteral("iunmap"), Mappings::InsertModeMapping); modeForMapCommand.insert(QStringLiteral("cunmap"), Mappings::CommandModeMapping); } return modeForMapCommand.value(mapCommand); } bool Commands::isMapCommandRecursive(const QString &mapCommand) { static QMap isMapCommandRecursive; if (isMapCommandRecursive.isEmpty()) { isMapCommandRecursive.insert(QStringLiteral("nmap"), true); isMapCommandRecursive.insert(QStringLiteral("nm"), true); isMapCommandRecursive.insert(QStringLiteral("vmap"), true); isMapCommandRecursive.insert(QStringLiteral("vm"), true); isMapCommandRecursive.insert(QStringLiteral("imap"), true); isMapCommandRecursive.insert(QStringLiteral("im"), true); isMapCommandRecursive.insert(QStringLiteral("cmap"), true); isMapCommandRecursive.insert(QStringLiteral("cm"), true); } return isMapCommandRecursive.value(mapCommand); } //END ViCommands //BEGIN SedReplace SedReplace *SedReplace::m_instance = nullptr; bool SedReplace::interactiveSedReplace(KTextEditor::ViewPrivate *, QSharedPointer interactiveSedReplace) { EmulatedCommandBar *emulatedCommandBar = m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar(); emulatedCommandBar->startInteractiveSearchAndReplace(interactiveSedReplace); return true; } //END SedReplace diff --git a/src/vimode/completionreplayer.cpp b/src/vimode/completionreplayer.cpp index f388409a..5bf0c3fa 100644 --- a/src/vimode/completionreplayer.cpp +++ b/src/vimode/completionreplayer.cpp @@ -1,155 +1,155 @@ /* * This file is part of the KDE libraries * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "completionreplayer.h" #include #include "katepartdebug.h" #include "kateview.h" #include "katedocument.h" #include "completionrecorder.h" #include "macrorecorder.h" #include "lastchangerecorder.h" #include #include using namespace KateVi; CompletionReplayer::CompletionReplayer(InputModeManager *viInputModeManager) : m_viInputModeManager(viInputModeManager) { } CompletionReplayer::~CompletionReplayer() { } void CompletionReplayer::start(const CompletionList &completions) { m_nextCompletionIndex.push(0); m_CompletionsToReplay.push(completions); } void CompletionReplayer::stop() { m_CompletionsToReplay.pop(); m_nextCompletionIndex.pop(); } void CompletionReplayer::replay() { const Completion completion = nextCompletion(); KTextEditor::ViewPrivate *m_view = m_viInputModeManager->view(); KTextEditor::DocumentPrivate *doc = m_view->doc(); // Find beginning of the word. KTextEditor::Cursor cursorPos = m_view->cursorPosition(); KTextEditor::Cursor wordStart = KTextEditor::Cursor::invalid(); if (!doc->characterAt(cursorPos).isLetterOrNumber() && doc->characterAt(cursorPos) != QLatin1Char('_')) { cursorPos.setColumn(cursorPos.column() - 1); } while (cursorPos.column() >= 0 && (doc->characterAt(cursorPos).isLetterOrNumber() || doc->characterAt(cursorPos) == QLatin1Char('_'))) { wordStart = cursorPos; cursorPos.setColumn(cursorPos.column() - 1); } // Find end of current word. cursorPos = m_view->cursorPosition(); KTextEditor::Cursor wordEnd = KTextEditor::Cursor(cursorPos.line(), cursorPos.column() - 1); while (cursorPos.column() < doc->lineLength(cursorPos.line()) && (doc->characterAt(cursorPos).isLetterOrNumber() || doc->characterAt(cursorPos) == QLatin1Char('_'))) { wordEnd = cursorPos; cursorPos.setColumn(cursorPos.column() + 1); } QString completionText = completion.completedText(); const KTextEditor::Range currentWord = KTextEditor::Range(wordStart, KTextEditor::Cursor(wordEnd.line(), wordEnd.column() + 1)); // Should we merge opening brackets? Yes, if completion is a function with arguments and after the cursor // there is (optional whitespace) followed by an open bracket. int offsetFinalCursorPosBy = 0; if (completion.completionType() == Completion::FunctionWithArgs) { const int nextMergableBracketAfterCursorPos = findNextMergeableBracketPos(currentWord.end()); if (nextMergableBracketAfterCursorPos != -1) { if (completionText.endsWith(QLatin1String("()"))) { // Strip "()". completionText = completionText.left(completionText.length() - 2); } else if (completionText.endsWith(QLatin1String("();"))) { // Strip "();". completionText = completionText.left(completionText.length() - 3); } // Ensure cursor ends up after the merged open bracket. offsetFinalCursorPosBy = nextMergableBracketAfterCursorPos + 1; } else { if (!completionText.endsWith(QLatin1String("()")) && !completionText.endsWith(QLatin1String("();"))) { // Original completion merged with an opening bracket; we'll have to add our own brackets. completionText.append(QLatin1String("()")); } // Position cursor correctly i.e. we'll have added "functionname()" or "functionname();"; need to step back by // one or two to be after the opening bracket. offsetFinalCursorPosBy = completionText.endsWith(QLatin1Char(';')) ? -2 : -1; } } KTextEditor::Cursor deleteEnd = completion.removeTail() ? currentWord.end() : KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + 0); if (currentWord.isValid()) { doc->removeText(KTextEditor::Range(currentWord.start(), deleteEnd)); doc->insertText(currentWord.start(), completionText); } else { doc->insertText(m_view->cursorPosition(), completionText); } if (offsetFinalCursorPosBy != 0) { m_view->setCursorPosition(KTextEditor::Cursor(m_view->cursorPosition().line(), m_view->cursorPosition().column() + offsetFinalCursorPosBy)); } if (!m_viInputModeManager->lastChangeRecorder()->isReplaying()) { Q_ASSERT(m_viInputModeManager->macroRecorder()->isReplaying()); // Post the completion back: it needs to be added to the "last change" list ... m_viInputModeManager->completionRecorder()->logCompletionEvent(completion); // ... but don't log the ctrl-space that led to this call to replayCompletion(), as // a synthetic ctrl-space was just added to the last change keypresses by logCompletionEvent(), and we don't // want to duplicate them! m_viInputModeManager->doNotLogCurrentKeypress(); } } Completion CompletionReplayer::nextCompletion() { Q_ASSERT(m_viInputModeManager->lastChangeRecorder()->isReplaying() || m_viInputModeManager->macroRecorder()->isReplaying()); if (m_nextCompletionIndex.top() >= m_CompletionsToReplay.top().length()) { qCDebug(LOG_KTE) << "Something wrong here: requesting more completions for macro than we actually have. Returning dummy."; return Completion(QString(), false, Completion::PlainText); } return m_CompletionsToReplay.top()[m_nextCompletionIndex.top()++]; } int CompletionReplayer::findNextMergeableBracketPos(const KTextEditor::Cursor &startPos) const { KTextEditor::DocumentPrivate *doc = m_viInputModeManager->view()->doc(); const QString lineAfterCursor = doc->text(KTextEditor::Range(startPos, KTextEditor::Cursor(startPos.line(), doc->lineLength(startPos.line())))); - static const QRegularExpression whitespaceThenOpeningBracket(QLatin1String("^\\s*(\\()")); + static const QRegularExpression whitespaceThenOpeningBracket(QStringLiteral("^\\s*(\\()")); QRegularExpressionMatch match = whitespaceThenOpeningBracket.match(lineAfterCursor); int nextMergableBracketAfterCursorPos = -1; if (match.hasMatch()) { nextMergableBracketAfterCursorPos = match.capturedStart(1); } return nextMergableBracketAfterCursorPos; } diff --git a/src/vimode/emulatedcommandbar/commandmode.cpp b/src/vimode/emulatedcommandbar/commandmode.cpp index 1ccca9b8..4ab2af38 100644 --- a/src/vimode/emulatedcommandbar/commandmode.cpp +++ b/src/vimode/emulatedcommandbar/commandmode.cpp @@ -1,408 +1,408 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2013-2016 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "commandmode.h" #include "emulatedcommandbar.h" #include "interactivesedreplacemode.h" #include "searchmode.h" #include "../commandrangeexpressionparser.h" #include #include #include #include "../globalstate.h" #include "../history.h" #include "katescriptmanager.h" #include "katecmds.h" #include #include #include #include using namespace KateVi; CommandMode::CommandMode ( EmulatedCommandBar* emulatedCommandBar, MatchHighlighter* matchHighlighter, InputModeManager* viInputModeManager, KTextEditor::ViewPrivate* view, QLineEdit* edit, InteractiveSedReplaceMode* interactiveSedReplaceMode, Completer* completer) : ActiveMode ( emulatedCommandBar, matchHighlighter, viInputModeManager, view), m_edit(edit), m_interactiveSedReplaceMode(interactiveSedReplaceMode), m_completer(completer) { QList cmds; cmds.push_back(KateCommands::CoreCommands::self()); cmds.push_back(Commands::self()); cmds.push_back(AppCommands::self()); cmds.push_back(SedReplace::self()); cmds.push_back(BufferCommands::self()); Q_FOREACH (KTextEditor::Command *cmd, KateScriptManager::self()->commandLineScripts()) { cmds.push_back(cmd); } Q_FOREACH (KTextEditor::Command *cmd, cmds) { QStringList l = cmd->cmds(); for (int z = 0; z < l.count(); z++) { m_cmdDict.insert(l[z], cmd); } m_cmdCompletion.insertItems(l); } } bool CommandMode::handleKeyPress ( const QKeyEvent* keyEvent ) { if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_D || keyEvent->key() == Qt::Key_F)) { CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); if (parsedSedExpression.parsedSuccessfully) { const bool clearFindTerm = (keyEvent->key() == Qt::Key_D); if (clearFindTerm) { m_edit->setSelection(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1); m_edit->insert(QString()); } else { // Clear replace term. m_edit->setSelection(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1); m_edit->insert(QString()); } } return true; } return false; } void CommandMode::editTextChanged ( const QString& newText ) { Q_UNUSED(newText); // We read the current text from m_edit. if (m_completer->isCompletionActive()) return; // Command completion doesn't need to be manually invoked. if (!withoutRangeExpression().isEmpty() && !m_completer->isNextTextChangeDueToCompletionChange()) { // ... However, command completion mode should not be automatically invoked if this is not the current leading // word in the text edit (it gets annoying if completion pops up after ":s/se" etc). const bool commandBeforeCursorIsLeading = (commandBeforeCursorBegin() == rangeExpression().length()); if (commandBeforeCursorIsLeading) { CompletionStartParams completionStartParams = activateCommandCompletion(); startCompletion(completionStartParams); } } } void CommandMode::deactivate ( bool wasAborted ) { if (wasAborted) { // Appending the command to the history when it is executed is handled elsewhere; we can't // do it inside closed() as we may still be showing the command response display. viInputModeManager()->globalState()->commandHistory()->append(m_edit->text()); // With Vim, aborting a command returns us to Normal mode, even if we were in Visual Mode. // If we switch from Visual to Normal mode, we need to clear the selection. view()->clearSelection(); } } CompletionStartParams CommandMode::completionInvoked(Completer::CompletionInvocation invocationType) { CompletionStartParams completionStartParams; if (invocationType == Completer::CompletionInvocation::ExtraContext) { if (isCursorInFindTermOfSed()) { completionStartParams = activateSedFindHistoryCompletion(); } else if (isCursorInReplaceTermOfSed()) { completionStartParams = activateSedReplaceHistoryCompletion(); } else { completionStartParams = activateCommandHistoryCompletion(); } } else { // Normal context, so boring, ordinary History completion. completionStartParams = activateCommandHistoryCompletion(); } return completionStartParams; } void CommandMode::completionChosen() { QString commandToExecute = m_edit->text(); CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); if (parsedSedExpression.parsedSuccessfully) { const QString originalFindTerm = sedFindTerm(); const QString convertedFindTerm = vimRegexToQtRegexPattern(originalFindTerm); const QString commandWithSedSearchRegexConverted = withSedFindTermReplacedWith(convertedFindTerm); viInputModeManager()->globalState()->searchHistory()->append(originalFindTerm); const QString replaceTerm = sedReplaceTerm(); viInputModeManager()->globalState()->replaceHistory()->append(replaceTerm); commandToExecute = commandWithSedSearchRegexConverted; } const QString commandResponseMessage = executeCommand(commandToExecute); // Don't close the bar if executing the command switched us to Interactive Sed Replace mode. if (!m_interactiveSedReplaceMode->isActive()) { if (commandResponseMessage.isEmpty()) { emulatedCommandBar()->hideMe(); } else { closeWithStatusMessage(commandResponseMessage); } } viInputModeManager()->globalState()->commandHistory()->append(m_edit->text()); } QString CommandMode::executeCommand ( const QString& commandToExecute ) { // Silently ignore leading space characters and colon characters (for vi-heads). uint n = 0; const uint textlen = commandToExecute.length(); while ((n < textlen) && commandToExecute[n].isSpace()) { n++; } if (n >= textlen) { return QString(); } QString commandResponseMessage; QString cmd = commandToExecute.mid(n); KTextEditor::Range range = CommandRangeExpressionParser(viInputModeManager()).parseRange(cmd, cmd); if (cmd.length() > 0) { KTextEditor::Command *p = queryCommand(cmd); if (p) { KateViCommandInterface *ci = dynamic_cast(p); if (ci) { ci->setViInputModeManager(viInputModeManager()); ci->setViGlobal(viInputModeManager()->globalState()); } // The following commands changes the focus themselves, so bar should be hidden before execution. // We got a range and a valid command, but the command does not support ranges. if (range.isValid() && !p->supportsRange(cmd)) { commandResponseMessage = i18n("Error: No range allowed for command \"%1\".", cmd); } else { if (p->exec(view(), cmd, commandResponseMessage, range)) { if (commandResponseMessage.length() > 0) { commandResponseMessage = i18n("Success: ") + commandResponseMessage; } } else { if (commandResponseMessage.length() > 0) { if (commandResponseMessage.contains(QLatin1Char('\n'))) { // multiline error, use widget with more space QWhatsThis::showText(emulatedCommandBar()->mapToGlobal(QPoint(0, 0)), commandResponseMessage); } } else { commandResponseMessage = i18n("Command \"%1\" failed.", cmd); } } } } else { commandResponseMessage = i18n("No such command: \"%1\"", cmd); } } // the following commands change the focus themselves - static const QRegularExpression reCmds(QLatin1String("^(buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$")); + static const QRegularExpression reCmds(QStringLiteral("^(buffer|b|new|vnew|bp|bprev|tabp|tabprev|bn|bnext|tabn|tabnext|bf|bfirst|tabf|tabfirst|bl|blast|tabl|tablast|e|edit|tabe|tabedit|tabnew)$")); if (!reCmds.match(cmd.split(QLatin1Char(' ')).at(0)).hasMatch()) { view()->setFocus(); } viInputModeManager()->reset(); return commandResponseMessage; } QString CommandMode::withoutRangeExpression() { const QString originalCommand = m_edit->text(); return originalCommand.mid(rangeExpression().length()); } QString CommandMode::rangeExpression() { const QString command = m_edit->text(); return CommandRangeExpressionParser(viInputModeManager()).parseRangeString(command); } CommandMode::ParsedSedExpression CommandMode::parseAsSedExpression() { const QString commandWithoutRangeExpression = withoutRangeExpression(); ParsedSedExpression parsedSedExpression; QString delimiter; parsedSedExpression.parsedSuccessfully = SedReplace::parse(commandWithoutRangeExpression, delimiter, parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos, parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos); if (parsedSedExpression.parsedSuccessfully) { parsedSedExpression.delimiter = delimiter.at(0); if (parsedSedExpression.replaceBeginPos == -1) { if (parsedSedExpression.findBeginPos != -1) { // The replace term was empty, and a quirk of the regex used is that replaceBeginPos will be -1. // It's actually the position after the first occurrence of the delimiter after the end of the find pos. parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.findEndPos) + 1; parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1; } else { // Both find and replace terms are empty; replace term is at the third occurrence of the delimiter. parsedSedExpression.replaceBeginPos = 0; for (int delimiterCount = 1; delimiterCount <= 3; delimiterCount++) { parsedSedExpression.replaceBeginPos = commandWithoutRangeExpression.indexOf(delimiter, parsedSedExpression.replaceBeginPos + 1); } parsedSedExpression.replaceEndPos = parsedSedExpression.replaceBeginPos - 1; } } if (parsedSedExpression.findBeginPos == -1) { // The find term was empty, and a quirk of the regex used is that findBeginPos will be -1. // It's actually the position after the first occurrence of the delimiter. parsedSedExpression.findBeginPos = commandWithoutRangeExpression.indexOf(delimiter) + 1; parsedSedExpression.findEndPos = parsedSedExpression.findBeginPos - 1; } } if (parsedSedExpression.parsedSuccessfully) { parsedSedExpression.findBeginPos += rangeExpression().length(); parsedSedExpression.findEndPos += rangeExpression().length(); parsedSedExpression.replaceBeginPos += rangeExpression().length(); parsedSedExpression.replaceEndPos += rangeExpression().length(); } return parsedSedExpression; } QString CommandMode::sedFindTerm() { const QString command = m_edit->text(); ParsedSedExpression parsedSedExpression = parseAsSedExpression(); Q_ASSERT(parsedSedExpression.parsedSuccessfully); return command.mid(parsedSedExpression.findBeginPos, parsedSedExpression.findEndPos - parsedSedExpression.findBeginPos + 1); } QString CommandMode::sedReplaceTerm() { const QString command = m_edit->text(); ParsedSedExpression parsedSedExpression = parseAsSedExpression(); Q_ASSERT(parsedSedExpression.parsedSuccessfully); return command.mid(parsedSedExpression.replaceBeginPos, parsedSedExpression.replaceEndPos - parsedSedExpression.replaceBeginPos + 1); } QString CommandMode::withSedFindTermReplacedWith ( const QString& newFindTerm ) { const QString command = m_edit->text(); ParsedSedExpression parsedSedExpression = parseAsSedExpression(); Q_ASSERT(parsedSedExpression.parsedSuccessfully); return command.mid(0, parsedSedExpression.findBeginPos) + newFindTerm + command.mid(parsedSedExpression.findEndPos + 1); } QString CommandMode::withSedDelimiterEscaped ( const QString& text ) { ParsedSedExpression parsedSedExpression = parseAsSedExpression(); QString delimiterEscaped = ensuredCharEscaped(text, parsedSedExpression.delimiter); return delimiterEscaped; } bool CommandMode::isCursorInFindTermOfSed() { ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return parsedSedExpression.parsedSuccessfully && (m_edit->cursorPosition() >= parsedSedExpression.findBeginPos && m_edit->cursorPosition() <= parsedSedExpression.findEndPos + 1); } bool CommandMode::isCursorInReplaceTermOfSed() { ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return parsedSedExpression.parsedSuccessfully && m_edit->cursorPosition() >= parsedSedExpression.replaceBeginPos && m_edit->cursorPosition() <= parsedSedExpression.replaceEndPos + 1; } int CommandMode::commandBeforeCursorBegin() { const QString textWithoutRangeExpression = withoutRangeExpression(); const int cursorPositionWithoutRangeExpression = m_edit->cursorPosition() - rangeExpression().length(); int commandBeforeCursorBegin = cursorPositionWithoutRangeExpression - 1; while (commandBeforeCursorBegin >= 0 && (textWithoutRangeExpression[commandBeforeCursorBegin].isLetterOrNumber() || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('_') || textWithoutRangeExpression[commandBeforeCursorBegin] == QLatin1Char('-'))) { commandBeforeCursorBegin--; } commandBeforeCursorBegin++; commandBeforeCursorBegin += rangeExpression().length(); return commandBeforeCursorBegin; } CompletionStartParams CommandMode::activateCommandCompletion() { return CompletionStartParams::createModeSpecific(m_cmdCompletion.items(), commandBeforeCursorBegin()); } CompletionStartParams CommandMode::activateCommandHistoryCompletion() { return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->commandHistory()->items()), 0); } CompletionStartParams CommandMode::activateSedFindHistoryCompletion() { if (viInputModeManager()->globalState()->searchHistory()->isEmpty()) { return CompletionStartParams::invalid(); } CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), parsedSedExpression.findBeginPos, [this] (const QString& completion) -> QString { return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion)); }); } CompletionStartParams CommandMode::activateSedReplaceHistoryCompletion() { if (viInputModeManager()->globalState()->replaceHistory()->isEmpty()) { return CompletionStartParams::invalid(); } CommandMode::ParsedSedExpression parsedSedExpression = parseAsSedExpression(); return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->replaceHistory()->items()), parsedSedExpression.replaceBeginPos, [this] (const QString& completion) -> QString { return withCaseSensitivityMarkersStripped(withSedDelimiterEscaped(completion)); }); } KTextEditor::Command* CommandMode::queryCommand ( const QString& cmd ) const { // a command can be named ".*[\w\-]+" with the constrain that it must // contain at least one letter. int f = 0; bool b = false; // special case: '-' and '_' can be part of a command name, but if the // command is 's' (substitute), it should be considered the delimiter and // should not be counted as part of the command name if (cmd.length() >= 2 && cmd.at(0) == QLatin1Char('s') && (cmd.at(1) == QLatin1Char('-') || cmd.at(1) == QLatin1Char('_'))) { return m_cmdDict.value(QStringLiteral("s")); } for (; f < cmd.length(); f++) { if (cmd[f].isLetter()) { b = true; } if (b && (! cmd[f].isLetterOrNumber() && cmd[f] != QLatin1Char('-') && cmd[f] != QLatin1Char('_'))) { break; } } return m_cmdDict.value(cmd.left(f)); } diff --git a/src/vimode/emulatedcommandbar/completer.cpp b/src/vimode/emulatedcommandbar/completer.cpp index 85dd2d32..2cc55c43 100644 --- a/src/vimode/emulatedcommandbar/completer.cpp +++ b/src/vimode/emulatedcommandbar/completer.cpp @@ -1,260 +1,260 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2013-2016 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "completer.h" #include "emulatedcommandbar.h" using namespace KateVi; #include "kateview.h" #include #include #include #include #include namespace { bool caseInsensitiveLessThan(const QString &s1, const QString &s2) { return s1.toLower() < s2.toLower(); } } Completer::Completer ( EmulatedCommandBar* emulatedCommandBar, KTextEditor::ViewPrivate* view, QLineEdit* edit ) : m_edit(edit) , m_view(view) { m_completer = new QCompleter(QStringList(), edit); // Can't find a way to stop the QCompleter from auto-completing when attached to a QLineEdit, // so don't actually set it as the QLineEdit's completer. m_completer->setWidget(edit); m_completer->setObjectName(QStringLiteral("completer")); m_completionModel = new QStringListModel(emulatedCommandBar); m_completer->setModel(m_completionModel); m_completer->setCaseSensitivity(Qt::CaseInsensitive); m_completer->popup()->installEventFilter(emulatedCommandBar); } void Completer::startCompletion ( const CompletionStartParams& completionStartParams ) { if (completionStartParams.completionType != CompletionStartParams::None) { m_completionModel->setStringList(completionStartParams.completions); const QString completionPrefix = m_edit->text().mid(completionStartParams.wordStartPos, m_edit->cursorPosition() - completionStartParams.wordStartPos); m_completer->setCompletionPrefix(completionPrefix); m_completer->complete(); m_currentCompletionStartParams = completionStartParams; m_currentCompletionType = completionStartParams.completionType; } } void Completer::deactivateCompletion() { m_completer->popup()->hide(); m_currentCompletionType = CompletionStartParams::None; } bool Completer::isCompletionActive() const { return m_currentCompletionType != CompletionStartParams::None; } bool Completer::isNextTextChangeDueToCompletionChange() const { return m_isNextTextChangeDueToCompletionChange; } bool Completer::completerHandledKeypress ( const QKeyEvent* keyEvent ) { if (!m_edit->isVisible()) return false; if (keyEvent->modifiers() == Qt::ControlModifier && (keyEvent->key() == Qt::Key_C || keyEvent->key() == Qt::Key_BracketLeft)) { if (m_currentCompletionType != CompletionStartParams::None && m_completer->popup()->isVisible()) { abortCompletionAndResetToPreCompletion(); return true; } } if (keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_Space) { CompletionStartParams completionStartParams = activateWordFromDocumentCompletion(); startCompletion(completionStartParams); return true; } if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_P) || keyEvent->key() == Qt::Key_Down) { if (!m_completer->popup()->isVisible()) { const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::ExtraContext); startCompletion(completionStartParams); if (m_currentCompletionType != CompletionStartParams::None) { setCompletionIndex(0); } } else { // Descend to next row, wrapping around if necessary. if (m_completer->currentRow() + 1 == m_completer->completionCount()) { setCompletionIndex(0); } else { setCompletionIndex(m_completer->currentRow() + 1); } } return true; } if ((keyEvent->modifiers() == Qt::ControlModifier && keyEvent->key() == Qt::Key_N) || keyEvent->key() == Qt::Key_Up) { if (!m_completer->popup()->isVisible()) { const CompletionStartParams completionStartParams = m_currentMode->completionInvoked(CompletionInvocation::NormalContext); startCompletion(completionStartParams); setCompletionIndex(m_completer->completionCount() - 1); } else { // Ascend to previous row, wrapping around if necessary. if (m_completer->currentRow() == 0) { setCompletionIndex(m_completer->completionCount() - 1); } else { setCompletionIndex(m_completer->currentRow() - 1); } } return true; } if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { if (!m_completer->popup()->isVisible() || m_currentCompletionType != CompletionStartParams::WordFromDocument) { m_currentMode->completionChosen(); } deactivateCompletion(); return true; } return false; } void Completer::editTextChanged ( const QString& newText ) { if (!m_isNextTextChangeDueToCompletionChange) { m_textToRevertToIfCompletionAborted = newText; m_cursorPosToRevertToIfCompletionAborted = m_edit->cursorPosition(); } // If we edit the text after having selected a completion, this means we implicitly accept it, // and so we should dismiss it. if (!m_isNextTextChangeDueToCompletionChange && m_completer->popup()->currentIndex().row() != -1) { deactivateCompletion(); } if (m_currentCompletionType != CompletionStartParams::None && !m_isNextTextChangeDueToCompletionChange) { updateCompletionPrefix(); } } void Completer::setCurrentMode ( ActiveMode* currentMode ) { m_currentMode = currentMode; } void Completer::setCompletionIndex ( int index ) { const QModelIndex modelIndex = m_completer->popup()->model()->index(index, 0); // Need to set both of these, for some reason. m_completer->popup()->setCurrentIndex(modelIndex); m_completer->setCurrentRow(index); m_completer->popup()->scrollTo(modelIndex); currentCompletionChanged(); } void Completer::currentCompletionChanged() { const QString newCompletion = m_completer->currentCompletion(); if (newCompletion.isEmpty()) { return; } QString transformedCompletion = newCompletion; if (m_currentCompletionStartParams.completionTransform) { transformedCompletion = m_currentCompletionStartParams.completionTransform(newCompletion); } m_isNextTextChangeDueToCompletionChange = true; m_edit->setSelection(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos); m_edit->insert(transformedCompletion); m_isNextTextChangeDueToCompletionChange = false; } void Completer::updateCompletionPrefix() { const QString completionPrefix = m_edit->text().mid(m_currentCompletionStartParams.wordStartPos, m_edit->cursorPosition() - m_currentCompletionStartParams.wordStartPos); m_completer->setCompletionPrefix(completionPrefix); // Seem to need a call to complete() else the size of the popup box is not altered appropriately. m_completer->complete(); } CompletionStartParams Completer::activateWordFromDocumentCompletion() { - static const QRegularExpression wordRegEx(QLatin1String("\\w{1,}")); + static const QRegularExpression wordRegEx(QStringLiteral("\\w{1,}")); QRegularExpressionMatch match; QStringList foundWords; // Narrow the range of lines we search around the cursor so that we don't die on huge files. const int startLine = qMax(0, m_view->cursorPosition().line() - 4096); const int endLine = qMin(m_view->document()->lines(), m_view->cursorPosition().line() + 4096); for (int lineNum = startLine; lineNum < endLine; lineNum++) { const QString line = m_view->document()->line(lineNum); int wordSearchBeginPos = 0; while ((match = wordRegEx.match(line, wordSearchBeginPos)).hasMatch()) { const QString foundWord = match.captured(); foundWords << foundWord; wordSearchBeginPos = match.capturedEnd(); } } foundWords = QSet::fromList(foundWords).toList(); std::sort(foundWords.begin(), foundWords.end(), caseInsensitiveLessThan); CompletionStartParams completionStartParams; completionStartParams.completionType = CompletionStartParams::WordFromDocument; completionStartParams.completions = foundWords; completionStartParams.wordStartPos = wordBeforeCursorBegin(); return completionStartParams; } QString Completer::wordBeforeCursor() { const int wordBeforeCursorBegin = this->wordBeforeCursorBegin(); return m_edit->text().mid(wordBeforeCursorBegin, m_edit->cursorPosition() - wordBeforeCursorBegin); } int Completer::wordBeforeCursorBegin() { int wordBeforeCursorBegin = m_edit->cursorPosition() - 1; while (wordBeforeCursorBegin >= 0 && (m_edit->text()[wordBeforeCursorBegin].isLetterOrNumber() || m_edit->text()[wordBeforeCursorBegin] == QLatin1Char('_'))) { wordBeforeCursorBegin--; } wordBeforeCursorBegin++; return wordBeforeCursorBegin; } void Completer::abortCompletionAndResetToPreCompletion() { deactivateCompletion(); m_isNextTextChangeDueToCompletionChange = true; m_edit->setText(m_textToRevertToIfCompletionAborted); m_edit->setCursorPosition(m_cursorPosToRevertToIfCompletionAborted); m_isNextTextChangeDueToCompletionChange = false; } diff --git a/src/vimode/modes/normalvimode.cpp b/src/vimode/modes/normalvimode.cpp index b02aa223..b53faf3f 100644 --- a/src/vimode/modes/normalvimode.cpp +++ b/src/vimode/modes/normalvimode.cpp @@ -1,4147 +1,4147 @@ /* This file is part of the KDE libraries and the Kate part. * * Copyright (C) 2008-2009 Erlend Hamberg * Copyright (C) 2008 Evgeniy Ivanov * Copyright (C) 2009 Paul Gideon Dann * Copyright (C) 2011 Svyatoslav Kuzmich * Copyright (C) 2012 - 2013 Simon St James * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katebuffer.h" #include "katecompletionwidget.h" #include "kateconfig.h" #include "kateglobal.h" #include "katepartdebug.h" #include "kateundomanager.h" #include #include #include "kateviewhelpers.h" #include "kateviewinternal.h" #include #include #include #include #include #include #include #include #include #include #include "katecmd.h" #include #include "kateviinputmode.h" #include #include #include #include #include #include #include #include using namespace KateVi; #define ADDCMD(STR, FUNC, FLGS) m_commands.push_back( \ new Command( this, QStringLiteral(STR), &NormalViMode::FUNC, FLGS ) ); #define ADDMOTION(STR, FUNC, FLGS) m_motions.push_back( \ new Motion( this, QStringLiteral(STR), &NormalViMode::FUNC, FLGS ) ); NormalViMode::NormalViMode(InputModeManager *viInputModeManager, KTextEditor::ViewPrivate *view, KateViewInternal *viewInternal) : ModeBase() { m_view = view; m_viewInternal = viewInternal; m_viInputModeManager = viInputModeManager; m_stickyColumn = -1; m_lastMotionWasVisualLineUpOrDown = false; m_currentMotionWasVisualLineUpOrDown = false; // FIXME: make configurable m_extraWordCharacters = QString(); m_matchingItems[QStringLiteral("/*")] = QStringLiteral("*/"); m_matchingItems[QStringLiteral("*/")] = QStringLiteral("-/*"); m_matchItemRegex = generateMatchingItemRegex(); m_scroll_count_limit = 1000; // Limit of count for scroll commands. initializeCommands(); m_pendingResetIsDueToExit = false; m_isRepeatedTFcommand = false; m_lastMotionWasLinewiseInnerBlock = false; m_motionCanChangeWholeVisualModeSelection = false; resetParser(); // initialise with start configuration m_isUndo = false; connect(doc()->undoManager(), SIGNAL(undoStart(KTextEditor::Document*)), this, SLOT(undoBeginning())); connect(doc()->undoManager(), SIGNAL(undoEnd(KTextEditor::Document*)), this, SLOT(undoEnded())); updateYankHighlightAttrib(); connect(view, SIGNAL(configChanged()), this, SLOT(updateYankHighlightAttrib())); connect(doc(), SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearYankHighlight())); connect(doc(), SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(aboutToDeleteMovingInterfaceContent())); } NormalViMode::~NormalViMode() { qDeleteAll(m_commands); qDeleteAll(m_motions); qDeleteAll(m_highlightedYanks); } /** * parses a key stroke to check if it's a valid (part of) a command * @return true if a command was completed and executed, false otherwise */ bool NormalViMode::handleKeypress(const QKeyEvent *e) { const int keyCode = e->key(); // ignore modifier keys alone if (keyCode == Qt::Key_Shift || keyCode == Qt::Key_Control || keyCode == Qt::Key_Alt || keyCode == Qt::Key_Meta) { return false; } clearYankHighlight(); if (keyCode == Qt::Key_Escape || (keyCode == Qt::Key_C && e->modifiers() == Qt::ControlModifier) || (keyCode == Qt::Key_BracketLeft && e->modifiers() == Qt::ControlModifier)) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); m_pendingResetIsDueToExit = true; // Vim in weird as if we e.g. i it claims (in the status bar) to still be in insert mode, // but behaves as if it's in normal mode. I'm treating the status bar thing as a bug and just exiting // insert mode altogether. m_viInputModeManager->setTemporaryNormalMode(false); reset(); return true; } const QChar key = KeyParser::self()->KeyEventToQChar(*e); const QChar lastChar = m_keys.isEmpty() ? QChar::Null : m_keys.at(m_keys.size() - 1); const bool waitingForRegisterOrCharToSearch = this->waitingForRegisterOrCharToSearch(); // Use replace caret when reading a character for "r" if (key == QLatin1Char('r') && !waitingForRegisterOrCharToSearch) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Underline); } m_keysVerbatim.append(KeyParser::self()->decodeKeySequence(key)); if ((keyCode >= Qt::Key_0 && keyCode <= Qt::Key_9 && lastChar != QLatin1Char('"')) // key 0-9 && (m_countTemp != 0 || keyCode != Qt::Key_0) // first digit can't be 0 && (!waitingForRegisterOrCharToSearch) // Not in the middle of "find char" motions or replacing char. && e->modifiers() == Qt::NoModifier) { m_countTemp *= 10; m_countTemp += keyCode - Qt::Key_0; return true; } else if (m_countTemp != 0) { m_count = getCount() * m_countTemp; m_countTemp = 0; m_iscounted = true; } m_keys.append(key); if (m_viInputModeManager->macroRecorder()->isRecording() && key == QLatin1Char('q')) { // Need to special case this "finish macro" q, as the "begin macro" q // needs a parameter whereas the finish macro does not. m_viInputModeManager->macroRecorder()->stop(); resetParser(); return true; } if ((key == QLatin1Char('/') || key == QLatin1Char('?')) && !waitingForRegisterOrCharToSearch) { // Special case for "/" and "?": these should be motions, but this is complicated by // the fact that the user must interact with the search bar before the range of the // motion can be determined. // We hack around this by showing the search bar immediately, and, when the user has // finished interacting with it, have the search bar send a "synthetic" keypresses // that will either abort everything (if the search was aborted) or "complete" the motion // otherwise. m_positionWhenIncrementalSearchBegan = m_view->cursorPosition(); if (key == QLatin1Char('/')) { commandSearchForward(); } else { commandSearchBackward(); } return true; } // Special case: "cw" and "cW" work the same as "ce" and "cE" if the cursor is // on a non-blank. This is because Vim interprets "cw" as change-word, and a // word does not include the following white space. (:help cw in vim) if ((m_keys == QLatin1String("cw") || m_keys == QLatin1String("cW")) && !getCharUnderCursor().isSpace()) { // Special case of the special case: :-) // If the cursor is at the end of the current word rewrite to "cl" const bool isWORD = (m_keys.at(1) == QLatin1Char('W')); const KTextEditor::Cursor currentPosition(m_view->cursorPosition()); const KTextEditor::Cursor endOfWordOrWORD = (isWORD ? findWORDEnd(currentPosition.line(), currentPosition.column() - 1, true) : findWordEnd(currentPosition.line(), currentPosition.column() - 1, true)); if (currentPosition == endOfWordOrWORD) { m_keys = QStringLiteral("cl"); } else { if (isWORD) { m_keys = QStringLiteral("cE"); } else { m_keys = QStringLiteral("ce"); } } } if (m_keys[ 0 ] == Qt::Key_QuoteDbl) { if (m_keys.size() < 2) { return true; // waiting for a register } else { QChar r = m_keys[ 1 ].toLower(); if ((r >= QLatin1Char('0') && r <= QLatin1Char('9')) || (r >= QLatin1Char('a') && r <= QLatin1Char('z')) || r == QLatin1Char('_') || r == QLatin1Char('+') || r == QLatin1Char('*') || r == QLatin1Char('#') || r == QLatin1Char('^')) { m_register = r; m_keys.clear(); return true; } else { resetParser(); return true; } } } // if we have any matching commands so far, check which ones still match if (!m_matchingCommands.isEmpty()) { int n = m_matchingCommands.size() - 1; // remove commands not matching anymore for (int i = n; i >= 0; i--) { if (!m_commands.at(m_matchingCommands.at(i))->matches(m_keys)) { if (m_commands.at(m_matchingCommands.at(i))->needsMotion()) { // "cache" command needing a motion for later m_motionOperatorIndex = m_matchingCommands.at(i); } m_matchingCommands.remove(i); } } // check if any of the matching commands need a motion/text object, if so // push the current command length to m_awaitingMotionOrTextObject so one // knows where to split the command between the operator and the motion for (int i = 0; i < m_matchingCommands.size(); i++) { if (m_commands.at(m_matchingCommands.at(i))->needsMotion()) { m_awaitingMotionOrTextObject.push(m_keys.size()); break; } } } else { // go through all registered commands and put possible matches in m_matchingCommands for (int i = 0; i < m_commands.size(); i++) { if (m_commands.at(i)->matches(m_keys)) { m_matchingCommands.push_back(i); if (m_commands.at(i)->needsMotion() && m_commands.at(i)->pattern().length() == m_keys.size()) { m_awaitingMotionOrTextObject.push(m_keys.size()); } } } } // this indicates where in the command string one should start looking for a motion command int checkFrom = (m_awaitingMotionOrTextObject.isEmpty() ? 0 : m_awaitingMotionOrTextObject.top()); // Use operator-pending caret when reading a motion for an operator // in normal mode. We need to check that we are indeed in normal mode // since visual mode inherits from it. if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode && !m_awaitingMotionOrTextObject.isEmpty()) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Half); } // look for matching motion commands from position 'checkFrom' // FIXME: if checkFrom hasn't changed, only motions whose index is in // m_matchingMotions should be checked bool motionExecuted = false; if (checkFrom < m_keys.size()) { for (int i = 0; i < m_motions.size(); i++) { if (m_motions.at(i)->matches(m_keys.mid(checkFrom))) { m_lastMotionWasLinewiseInnerBlock = false; m_matchingMotions.push_back(i); // if it matches exact, we have found the motion command to execute if (m_motions.at(i)->matchesExact(m_keys.mid(checkFrom))) { m_currentMotionWasVisualLineUpOrDown = false; motionExecuted = true; if (checkFrom == 0) { // no command given before motion, just move the cursor to wherever // the motion says it should go to Range r = m_motions.at(i)->execute(); m_motionCanChangeWholeVisualModeSelection = m_motions.at(i)->canChangeWholeVisualModeSelection(); // jump over folding regions since we are just moving the cursor int currLine = m_view->cursorPosition().line(); int delta = r.endLine - currLine; int vline = m_view->textFolding().lineToVisibleLine(currLine); r.endLine = m_view->textFolding().visibleLineToLine(qMax(vline + delta, 0) /* ensure we have a valid line */); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } // make sure the position is valid before moving the cursor there // TODO: can this be simplified? :/ if (r.valid && r.endLine >= 0 && (r.endLine == 0 || r.endLine <= doc()->lines() - 1) && r.endColumn >= 0) { if (r.endColumn >= doc()->lineLength(r.endLine) && doc()->lineLength(r.endLine) > 0) { r.endColumn = doc()->lineLength(r.endLine) - 1; } goToPos(r); // in the case of VisualMode we need to remember the motion commands as well. if (!m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->clearCurrentChangeLog(); } } else { qCDebug(LOG_KTE) << "Invalid position: (" << r.endLine << "," << r.endColumn << ")"; } resetParser(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } m_lastMotionWasVisualLineUpOrDown = m_currentMotionWasVisualLineUpOrDown; break; } else { // execute the specified command and supply the position returned from // the motion m_commandRange = m_motions.at(i)->execute(); m_linewiseCommand = m_motions.at(i)->isLineWise(); // if we didn't get an explicit start position, use the current cursor position if (m_commandRange.startLine == -1) { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); } // special case: When using the "w" motion in combination with an operator and // the last word moved over is at the end of a line, the end of that word // becomes the end of the operated text, not the first word in the next line. if (m_motions.at(i)->pattern() == QLatin1String("w") || m_motions.at(i)->pattern() == QLatin1String("W")) { if (m_commandRange.endLine != m_commandRange.startLine && m_commandRange.endColumn == getFirstNonBlank(m_commandRange.endLine)) { m_commandRange.endLine--; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine); } } m_commandWithMotion = true; if (m_commandRange.valid) { executeCommand(m_commands.at(m_motionOperatorIndex)); } else { qCDebug(LOG_KTE) << "Invalid range: " << "from (" << m_commandRange.startLine << "," << m_commandRange.startColumn << ")" << "to (" << m_commandRange.endLine << "," << m_commandRange.endColumn << ")"; } if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } m_commandWithMotion = false; reset(); break; } } } } } if (this->waitingForRegisterOrCharToSearch()) { // If we are waiting for a char to search or a new register, // don't translate next character; we need the actual character so that e.g. // 'ab' is translated to 'fb' if the mappings 'a' -> 'f' and 'b' -> something else // exist. m_viInputModeManager->keyMapper()->setDoNotMapNextKeypress(); } if (motionExecuted) { return true; } // if we have only one match, check if it is a perfect match and if so, execute it // if it's not waiting for a motion or a text object if (m_matchingCommands.size() == 1) { if (m_commands.at(m_matchingCommands.at(0))->matchesExact(m_keys) && !m_commands.at(m_matchingCommands.at(0))->needsMotion()) { if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } Command *cmd = m_commands.at(m_matchingCommands.at(0)); executeCommand(cmd); // check if reset() should be called. some commands in visual mode should not end visual mode if (cmd->shouldReset()) { reset(); m_view->setBlockSelection(false); } resetParser(); return true; } } else if (m_matchingCommands.size() == 0 && m_matchingMotions.size() == 0) { resetParser(); // A bit ugly: we haven't made use of the key event, // but don't want "typeable" keypresses (e.g. a, b, 3, etc) to be marked // as unused as they will then be added to the document, but we don't // want to swallow all keys in case this was a shortcut. // So say we made use of it if and only if it was *not* a shortcut. return e->type() != QEvent::ShortcutOverride; } m_matchingMotions.clear(); return true; // TODO - need to check this - it's currently required for making tests pass, but seems odd. } /** * (re)set to start configuration. This is done when a command is completed * executed or when a command is aborted */ void NormalViMode::resetParser() { m_keys.clear(); m_keysVerbatim.clear(); m_count = 0; m_oneTimeCountOverride = -1; m_iscounted = false; m_countTemp = 0; m_register = QChar::Null; m_findWaitingForChar = false; m_matchingCommands.clear(); m_matchingMotions.clear(); m_awaitingMotionOrTextObject.clear(); m_motionOperatorIndex = 0; m_commandWithMotion = false; m_linewiseCommand = true; m_deleteCommand = false; m_commandShouldKeepSelection = false; m_currentChangeEndMarker = KTextEditor::Cursor::invalid(); if(m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { m_viInputModeManager->inputAdapter()->setCaretStyle(KateRenderer::Block); } } // reset the command parser void NormalViMode::reset() { resetParser(); m_commandRange.startLine = -1; m_commandRange.startColumn = -1; } void NormalViMode::beginMonitoringDocumentChanges() { connect(doc(), &KTextEditor::DocumentPrivate::textInserted, this, &NormalViMode::textInserted); connect(doc(), &KTextEditor::DocumentPrivate::textRemoved, this, &NormalViMode::textRemoved); } void NormalViMode::executeCommand(const Command *cmd) { const ViMode originalViMode = m_viInputModeManager->getCurrentViMode(); cmd->execute(); // if normal mode was started by using Ctrl-O in insert mode, // it's time to go back to insert mode. if (m_viInputModeManager->getTemporaryNormalMode()) { startInsertMode(); m_viewInternal->repaint(); } // if the command was a change, and it didn't enter insert mode, store the key presses so that // they can be repeated with '.' if (m_viInputModeManager->getCurrentViMode() != ViMode::InsertMode && m_viInputModeManager->getCurrentViMode() != ViMode::ReplaceMode) { if (cmd->isChange() && !m_viInputModeManager->lastChangeRecorder()->isReplaying()) { m_viInputModeManager->storeLastChangeCommand(); } // when we transition to visual mode, remember the command in the keys history (V, v, ctrl-v, ...) // this will later result in buffer filled with something like this "Vjj>" which we can use later with repeat "." const bool commandSwitchedToVisualMode = ((originalViMode == ViMode::NormalMode) && m_viInputModeManager->isAnyVisualMode()); if (!commandSwitchedToVisualMode) { m_viInputModeManager->clearCurrentChangeLog(); } } // make sure the cursor does not end up after the end of the line KTextEditor::Cursor c(m_view->cursorPosition()); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { int lineLength = doc()->lineLength(c.line()); if (c.column() >= lineLength) { if (lineLength == 0) { c.setColumn(0); } else { c.setColumn(lineLength - 1); } } updateCursor(c); } } //////////////////////////////////////////////////////////////////////////////// // COMMANDS AND OPERATORS //////////////////////////////////////////////////////////////////////////////// /** * enter insert mode at the cursor position */ bool NormalViMode::commandEnterInsertMode() { m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode after the current character */ bool NormalViMode::commandEnterInsertModeAppend() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(c.column() + 1); // if empty line, the cursor should start at column 0 if (doc()->lineLength(c.line()) == 0) { c.setColumn(0); } // cursor should never be in a column > number of columns if (c.column() > doc()->lineLength(c.line())) { c.setColumn(doc()->lineLength(c.line())); } updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * start insert mode after the last character of the line */ bool NormalViMode::commandEnterInsertModeAppendEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } bool NormalViMode::commandEnterInsertModeBeforeFirstNonBlankInLine() { KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); cursor.setColumn(c); updateCursor(cursor); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setCount(getCount()); return startInsertMode(); } /** * enter insert mode at the last insert position */ bool NormalViMode::commandEnterInsertModeLast() { KTextEditor::Cursor c = m_viInputModeManager->marks()->getInsertStopped(); if (c.isValid()) { updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandEnterVisualLineMode() { if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { reset(); return true; } return startVisualLineMode(); } bool NormalViMode::commandEnterVisualBlockMode() { if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { reset(); return true; } return startVisualBlockMode(); } bool NormalViMode::commandReselectVisual() { // start last visual mode and set start = `< and cursor = `> KTextEditor::Cursor c1 = m_viInputModeManager->marks()->getSelectionStart(); KTextEditor::Cursor c2 = m_viInputModeManager->marks()->getSelectionFinish(); // we should either get two valid cursors or two invalid cursors Q_ASSERT(c1.isValid() == c2.isValid()); if (c1.isValid() && c2.isValid()) { m_viInputModeManager->getViVisualMode()->setStart(c1); bool returnValue = false; switch (m_viInputModeManager->getViVisualMode()->getLastVisualMode()) { case ViMode::VisualMode: returnValue = commandEnterVisualMode(); break; case ViMode::VisualLineMode: returnValue = commandEnterVisualLineMode(); break; case ViMode::VisualBlockMode: returnValue = commandEnterVisualBlockMode(); break; default: Q_ASSERT("invalid visual mode"); } m_viInputModeManager->getViVisualMode()->goToPos(c2); return returnValue; } else { error(QStringLiteral("No previous visual selection")); } return false; } bool NormalViMode::commandEnterVisualMode() { if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode) { reset(); return true; } return startVisualMode(); } bool NormalViMode::commandToOtherEnd() { if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->switchStartEnd(); return true; } return false; } bool NormalViMode::commandEnterReplaceMode() { m_stickyColumn = -1; m_viInputModeManager->getViReplaceMode()->setCount(getCount()); return startReplaceMode(); } bool NormalViMode::commandDeleteLine() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r; r.startLine = c.line(); r.endLine = c.line() + getCount() - 1; int column = c.column(); bool ret = deleteRange(r, LineWise); c = m_view->cursorPosition(); if (column > doc()->lineLength(c.line()) - 1) { column = doc()->lineLength(c.line()) - 1; } if (column < 0) { column = 0; } if (c.line() > doc()->lines() - 1) { c.setLine(doc()->lines() - 1); } c.setColumn(column); m_stickyColumn = -1; updateCursor(c); m_deleteCommand = true; return ret; } bool NormalViMode::commandDelete() { m_deleteCommand = true; return deleteRange(m_commandRange, getOperationMode()); } bool NormalViMode::commandDeleteToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = CharWise; m_commandRange.endColumn = KateVi::EOL; switch (m_viInputModeManager->getCurrentViMode()) { case ViMode::NormalMode: m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); m_commandRange.endLine = c.line() + getCount() - 1; break; case ViMode::VisualMode: case ViMode::VisualLineMode: m = LineWise; break; case ViMode::VisualBlockMode: m_commandRange.normalize(); m = Block; break; default: /* InsertMode and ReplaceMode will never call this method. */ Q_ASSERT(false); } bool r = deleteRange(m_commandRange, m); switch (m) { case CharWise: c.setColumn(doc()->lineLength(c.line()) - 1); break; case LineWise: c.setLine(m_commandRange.startLine); c.setColumn(getFirstNonBlank(qMin(doc()->lastLine(), m_commandRange.startLine))); break; case Block: c.setLine(m_commandRange.startLine); c.setColumn(m_commandRange.startColumn - 1); break; } // make sure cursor position is valid after deletion if (c.line() < 0) { c.setLine(0); } if (c.line() > doc()->lastLine()) { c.setLine(doc()->lastLine()); } if (c.column() > doc()->lineLength(c.line()) - 1) { c.setColumn(doc()->lineLength(c.line()) - 1); } if (c.column() < 0) { c.setColumn(0); } updateCursor(c); m_deleteCommand = true; return r; } bool NormalViMode::commandMakeLowercase() { KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text = text.left(text.size() - 1); // don't need '\n' at the end; } QString lowerCase = text.toLower(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, lowerCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeLowercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeLowercase(); } bool NormalViMode::commandMakeUppercase() { if (!m_commandRange.valid) { return false; } KTextEditor::Cursor c = m_view->cursorPosition(); OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text = text.left(text.size() - 1); // don't need '\n' at the end; } QString upperCase = text.toUpper(); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); doc()->replaceText(range, upperCase, m == Block); if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(start); } else { updateCursor(c); } return true; } bool NormalViMode::commandMakeUppercaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; return commandMakeUppercase(); } bool NormalViMode::commandChangeCase() { switchView(); QString text; KTextEditor::Range range; KTextEditor::Cursor c(m_view->cursorPosition()); // in visual mode, the range is from start position to end position... if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualMode || m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(c2.column() + 1); } else { c.setColumn(c.column() + 1); } range.setRange(c, c2); // ... in visual line mode, the range is from column 0 on the first line to // the line length of the last line... } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode) { KTextEditor::Cursor c2 = m_viInputModeManager->getViVisualMode()->getStart(); if (c2 > c) { c2.setColumn(doc()->lineLength(c2.line())); c.setColumn(0); } else { c.setColumn(doc()->lineLength(c.line())); c2.setColumn(0); } range.setRange(c, c2); // ... and in normal mode the range is from the current position to the // current position + count } else { KTextEditor::Cursor c2 = c; c2.setColumn(c.column() + getCount()); if (c2.column() > doc()->lineLength(c.line())) { c2.setColumn(doc()->lineLength(c.line())); } range.setRange(c, c2); } bool block = m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode; // get the text the command should operate on text = doc()->text(range, block); // for every character, switch its case for (int i = 0; i < text.length(); i++) { if (text.at(i).isUpper()) { text[i] = text.at(i).toLower(); } else if (text.at(i).isLower()) { text[i] = text.at(i).toUpper(); } } // replace the old text with the modified text doc()->replaceText(range, text, block); // in normal mode, move the cursor to the right, in visual mode move the // cursor to the start of the selection if (m_viInputModeManager->getCurrentViMode() == ViMode::NormalMode) { updateCursor(range.end()); } else { updateCursor(range.start()); } return true; } bool NormalViMode::commandChangeCaseRange() { OperationMode m = getOperationMode(); QString changedCase = getRange(m_commandRange, m); if (m == LineWise) { changedCase = changedCase.left(changedCase.size() - 1); // don't need '\n' at the end; } KTextEditor::Range range = KTextEditor::Range(m_commandRange.startLine, m_commandRange.startColumn, m_commandRange.endLine, m_commandRange.endColumn); // get the text the command should operate on // for every character, switch its case for (int i = 0; i < changedCase.length(); i++) { if (changedCase.at(i).isUpper()) { changedCase[i] = changedCase.at(i).toLower(); } else if (changedCase.at(i).isLower()) { changedCase[i] = changedCase.at(i).toUpper(); } } doc()->replaceText(range, changedCase, m == Block); return true; } bool NormalViMode::commandChangeCaseLine() { KTextEditor::Cursor c(m_view->cursorPosition()); if (doc()->lineLength(c.line()) == 0) { // Nothing to do. return true; } m_commandRange.startLine = c.line(); m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.startColumn = 0; m_commandRange.endColumn = doc()->lineLength(c.line()) - 1; // -1 is for excluding '\0' if (!commandChangeCaseRange()) { return false; } KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); if (getCount() > 1) { updateCursor(c); } else { updateCursor(start); } return true; } bool NormalViMode::commandOpenNewLineUnder() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); doc()->newLine(m_view); m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandOpenNewLineOver() { doc()->setUndoMergeAllEdits(true); KTextEditor::Cursor c(m_view->cursorPosition()); if (c.line() == 0) { doc()->insertLine(0, QString()); c.setColumn(0); c.setLine(0); updateCursor(c); } else { c.setLine(c.line() - 1); c.setColumn(getLine(c.line()).length()); updateCursor(c); doc()->newLine(m_view); } m_stickyColumn = -1; startInsertMode(); m_viInputModeManager->getViInsertMode()->setCount(getCount()); m_viInputModeManager->getViInsertMode()->setCountedRepeatsBeginOnNewLine(true); return true; } bool NormalViMode::commandJoinLines() { KTextEditor::Cursor c(m_view->cursorPosition()); unsigned int from = c.line(); unsigned int to = c.line() + ((getCount() == 1) ? 1 : getCount() - 1); // if we were given a range of lines, this information overrides the previous if (m_commandRange.startLine != -1 && m_commandRange.endLine != -1) { m_commandRange.normalize(); c.setLine(m_commandRange.startLine); from = m_commandRange.startLine; to = m_commandRange.endLine; } if (to >= (unsigned int)doc()->lines()) { return false; } bool nonEmptyLineFound = false; for (unsigned int lineNum = from; lineNum <= to; lineNum++) { if (!doc()->line(lineNum).isEmpty()) { nonEmptyLineFound = true; } } const int firstNonWhitespaceOnLastLine = doc()->kateTextLine(to)->firstChar(); QString leftTrimmedLastLine; if (firstNonWhitespaceOnLastLine != -1) { leftTrimmedLastLine = doc()->line(to).mid(firstNonWhitespaceOnLastLine); } joinLines(from, to); if (nonEmptyLineFound && leftTrimmedLastLine.isEmpty()) { // joinLines won't have added a trailing " ", whereas Vim does - follow suit. - doc()->insertText(KTextEditor::Cursor(from, doc()->lineLength(from)), QLatin1String(" ")); + doc()->insertText(KTextEditor::Cursor(from, doc()->lineLength(from)), QStringLiteral(" ")); } // Position cursor just before first non-whitesspace character of what was the last line joined. c.setColumn(doc()->lineLength(from) - leftTrimmedLastLine.length() - 1); if (c.column() >= 0) { updateCursor(c); } m_deleteCommand = true; return true; } bool NormalViMode::commandChange() { KTextEditor::Cursor c(m_view->cursorPosition()); OperationMode m = getOperationMode(); doc()->setUndoMergeAllEdits(true); commandDelete(); if (m == LineWise) { // if we deleted several lines, insert an empty line and put the cursor there. doc()->insertLine(m_commandRange.startLine, QString()); c.setLine(m_commandRange.startLine); c.setColumn(0); } else if (m == Block) { // block substitute can be simulated by first deleting the text // (done above) and then starting block prepend. return commandPrependToBlock(); } else { if (m_commandRange.startLine < m_commandRange.endLine) { c.setLine(m_commandRange.startLine); } c.setColumn(m_commandRange.startColumn); } updateCursor(c); setCount(0); // The count was for the motion, not the insertion. commandEnterInsertMode(); // correct indentation level if (m == LineWise) { m_view->align(); } m_deleteCommand = true; return true; } bool NormalViMode::commandChangeToEOL() { commandDeleteToEOL(); if (getOperationMode() == Block) { return commandPrependToBlock(); } m_deleteCommand = true; return commandEnterInsertModeAppend(); } bool NormalViMode::commandChangeLine() { m_deleteCommand = true; KTextEditor::Cursor c(m_view->cursorPosition()); c.setColumn(0); updateCursor(c); doc()->setUndoMergeAllEdits(true); // if count >= 2 start by deleting the whole lines if (getCount() >= 2) { Range r(c.line(), 0, c.line() + getCount() - 2, 0, InclusiveMotion); deleteRange(r); } // ... then delete the _contents_ of the last line, but keep the line Range r(c.line(), c.column(), c.line(), doc()->lineLength(c.line()) - 1, InclusiveMotion); deleteRange(r, CharWise, true); // ... then enter insert mode if (getOperationMode() == Block) { return commandPrependToBlock(); } commandEnterInsertModeAppend(); // correct indentation level m_view->align(); return true; } bool NormalViMode::commandSubstituteChar() { if (commandDeleteChar()) { // The count is only used for deletion of chars; the inserted text is not repeated setCount(0); return commandEnterInsertMode(); } m_deleteCommand = true; return false; } bool NormalViMode::commandSubstituteLine() { m_deleteCommand = true; return commandChangeLine(); } bool NormalViMode::commandYank() { bool r = false; QString yankedText; OperationMode m = getOperationMode(); yankedText = getRange(m_commandRange, m); highlightYank(m_commandRange, m); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return r; } bool NormalViMode::commandYankLine() { KTextEditor::Cursor c(m_view->cursorPosition()); QString lines; int linenum = c.line(); for (int i = 0; i < getCount(); i++) { lines.append(getLine(linenum + i) + QLatin1Char('\n')); } Range yankRange(linenum, 0, linenum + getCount() - 1, getLine(linenum + getCount() - 1).length(), InclusiveMotion); highlightYank(yankRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, lines, LineWise); yankToClipBoard(chosen_register, lines); return true; } bool NormalViMode::commandYankToEOL() { OperationMode m = CharWise; KTextEditor::Cursor c(m_view->cursorPosition()); MotionType motion = m_commandRange.motionType; m_commandRange.endLine = c.line() + getCount() - 1; m_commandRange.endColumn = doc()->lineLength(m_commandRange.endLine) - 1; m_commandRange.motionType = InclusiveMotion; switch (m_viInputModeManager->getCurrentViMode()) { case ViMode::NormalMode: m_commandRange.startLine = c.line(); m_commandRange.startColumn = c.column(); break; case ViMode::VisualMode: case ViMode::VisualLineMode: m = LineWise; { VisualViMode *visual = static_cast(this); visual->setStart(KTextEditor::Cursor(visual->getStart().line(), 0)); } break; case ViMode::VisualBlockMode: m = Block; break; default: /* InsertMode and ReplaceMode will never call this method. */ Q_ASSERT(false); } const QString &yankedText = getRange(m_commandRange, m); m_commandRange.motionType = motion; highlightYank(m_commandRange); QChar chosen_register = getChosenRegister(ZeroRegister); fillRegister(chosen_register, yankedText, m); yankToClipBoard(chosen_register, yankedText); return true; } // Insert the text in the given register after the cursor position. // This is the non-g version of paste, so the cursor will usually // end up on the last character of the pasted text, unless the text // was multi-line or linewise in which case it will end up // on the *first* character of the pasted text(!) // If linewise, will paste after the current line. bool NormalViMode::commandPaste() { return paste(AfterCurrentPosition, false, false); } // As with commandPaste, except that the text is pasted *at* the cursor position bool NormalViMode::commandPasteBefore() { return paste(AtCurrentPosition, false, false); } // As with commandPaste, except that the cursor will generally be placed *after* the // last pasted character (assuming the last pasted character is not at the end of the line). // If linewise, cursor will be at the beginning of the line *after* the last line of pasted text, // unless that line is the last line of the document; then it will be placed at the beginning of the // last line pasted. bool NormalViMode::commandgPaste() { return paste(AfterCurrentPosition, true, false); } // As with commandgPaste, except that it pastes *at* the current cursor position or, if linewise, // at the current line. bool NormalViMode::commandgPasteBefore() { return paste(AtCurrentPosition, true, false); } bool NormalViMode::commandIndentedPaste() { return paste(AfterCurrentPosition, false, true); } bool NormalViMode::commandIndentedPasteBefore() { return paste(AtCurrentPosition, false, true); } bool NormalViMode::commandDeleteChar() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column(), c.line(), c.column() + getCount(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.endColumn > doc()->lineLength(r.startLine)) { r.endColumn = doc()->lineLength(r.startLine); } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandDeleteCharBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c.line(), c.column() - getCount(), c.line(), c.column(), ExclusiveMotion); if (m_commandRange.startLine != -1 && m_commandRange.startColumn != -1) { r = m_commandRange; } else { if (r.startColumn < 0) { r.startColumn = 0; } } // should delete entire lines if in visual line mode and selection in visual block mode OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == VisualLineMode) { m = LineWise; } else if (m_viInputModeManager->getCurrentViMode() == VisualBlockMode) { m = Block; } m_deleteCommand = true; return deleteRange(r, m); } bool NormalViMode::commandReplaceCharacter() { QString key = KeyParser::self()->decodeKeySequence(m_keys.right(1)); // Filter out some special keys. const int keyCode = KeyParser::self()->encoded2qt(m_keys.right(1)); switch (keyCode) { case Qt::Key_Left: case Qt::Key_Right: case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Delete: case Qt::Key_Insert: case Qt::Key_Backspace: case Qt::Key_CapsLock: return true; case Qt::Key_Return: case Qt::Key_Enter: key = QStringLiteral("\n"); } bool r; if (m_viInputModeManager->isAnyVisualMode()) { OperationMode m = getOperationMode(); QString text = getRange(m_commandRange, m); if (m == LineWise) { text = text.left(text.size() - 1); // don't need '\n' at the end; } text.replace(QRegExp(QLatin1String("[^\n]")), key); m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, m_commandRange.startColumn); KTextEditor::Cursor end(m_commandRange.endLine, m_commandRange.endColumn); KTextEditor::Range range(start, end); r = doc()->replaceText(range, text, m == Block); } else { KTextEditor::Cursor c1(m_view->cursorPosition()); KTextEditor::Cursor c2(m_view->cursorPosition()); c2.setColumn(c2.column() + getCount()); if (c2.column() > doc()->lineLength(m_view->cursorPosition().line())) { return false; } r = doc()->replaceText(KTextEditor::Range(c1, c2), key.repeated(getCount())); updateCursor(c1); } return r; } bool NormalViMode::commandSwitchToCmdLine() { QString initialText; if (m_viInputModeManager->isAnyVisualMode()) { // if in visual mode, make command range == visual selection m_viInputModeManager->getViVisualMode()->saveRangeMarks(); initialText = QStringLiteral("'<,'>"); } else if (getCount() != 1) { // if a count is given, the range [current line] to [current line] + // count should be prepended to the command line initialText = QLatin1String(".,.+") + QString::number(getCount() - 1); } m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::Command, initialText); m_commandShouldKeepSelection = true; return true; } bool NormalViMode::commandSearchBackward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchBackward); return true; } bool NormalViMode::commandSearchForward() { m_viInputModeManager->inputAdapter()->showViModeEmulatedCommandBar(); m_viInputModeManager->inputAdapter()->viModeEmulatedCommandBar()->init(EmulatedCommandBar::SearchForward); return true; } bool NormalViMode::commandUndo() { // See BUG #328277 m_viInputModeManager->clearCurrentChangeLog(); if (doc()->undoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) doc()->editEnd(); doc()->undo(); if (mapped) doc()->editBegin(); if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandRedo() { if (doc()->redoCount() > 0) { const bool mapped = m_viInputModeManager->keyMapper()->isExecutingMapping(); if (mapped) doc()->editEnd(); doc()->redo(); if (mapped) doc()->editBegin(); if (m_viInputModeManager->isAnyVisualMode()) { m_viInputModeManager->getViVisualMode()->setStart(KTextEditor::Cursor(-1, -1)); m_view->clearSelection(); startNormalMode(); } return true; } return false; } bool NormalViMode::commandSetMark() { KTextEditor::Cursor c(m_view->cursorPosition()); QChar mark = m_keys.at(m_keys.size() - 1); m_viInputModeManager->marks()->setUserMark(mark, c); return true; } bool NormalViMode::commandIndentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), 1); return true; } bool NormalViMode::commandUnindentLine() { KTextEditor::Cursor c(m_view->cursorPosition()); doc()->indent(KTextEditor::Range(c.line(), 0, c.line() + getCount(), 0), -1); return true; } bool NormalViMode::commandIndentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; int col = getLine(line2).length(); doc()->indent(KTextEditor::Range(line1, 0, line2, col), getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandUnindentLines() { const bool downwards = m_commandRange.startLine < m_commandRange.endLine; m_commandRange.normalize(); int line1 = m_commandRange.startLine; int line2 = m_commandRange.endLine; doc()->indent(KTextEditor::Range(line1, 0, line2, doc()->lineLength(line2)), -getCount()); if (downwards) { updateCursor(KTextEditor::Cursor(m_commandRange.startLine, m_commandRange.startColumn)); } else { updateCursor(KTextEditor::Cursor(m_commandRange.endLine, m_commandRange.endColumn)); } return true; } bool NormalViMode::commandScrollPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageDown(); } } return true; } bool NormalViMode::commandScrollPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_view->pageUp(); } } return true; } bool NormalViMode::commandScrollHalfPageUp() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageUp(false, true); } } return true; } bool NormalViMode::commandScrollHalfPageDown() { if (getCount() < m_scroll_count_limit) { for (int i = 0; i < getCount(); i++) { m_viewInternal->pageDown(false, true); } } return true; } bool NormalViMode::commandCenterView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine() + linesDisplayed() / 2; const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandCenterViewOnNonBlank() { return commandCenterView(true); } bool NormalViMode::commandCenterViewOnCursor() { return commandCenterView(false); } bool NormalViMode::commandTopView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->startLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandTopViewOnNonBlank() { return commandTopView(true); } bool NormalViMode::commandTopViewOnCursor() { return commandTopView(false); } bool NormalViMode::commandBottomView(bool onFirst) { KTextEditor::Cursor c(m_view->cursorPosition()); const int virtualCenterLine = m_viewInternal->endLine(); const int virtualCursorLine = m_view->textFolding().lineToVisibleLine(c.line()); scrollViewLines(virtualCursorLine - virtualCenterLine); if (onFirst) { c.setColumn(getFirstNonBlank()); updateCursor(c); } return true; } bool NormalViMode::commandBottomViewOnNonBlank() { return commandBottomView(true); } bool NormalViMode::commandBottomViewOnCursor() { return commandBottomView(false); } bool NormalViMode::commandAbort() { m_pendingResetIsDueToExit = true; reset(); return true; } bool NormalViMode::commandPrintCharacterCode() { QChar ch = getCharUnderCursor(); if (ch == QChar::Null) { message(QStringLiteral("NUL")); } else { int code = ch.unicode(); QString dec = QString::number(code); QString hex = QString::number(code, 16); QString oct = QString::number(code, 8); if (oct.length() < 3) { oct.prepend(QLatin1Char('0')); } if (code > 0x80 && code < 0x1000) { hex.prepend((code < 0x100 ? QLatin1String("00") : QLatin1String("0"))); } message(i18n("'%1' %2, Hex %3, Octal %4", ch, dec, hex, oct)); } return true; } bool NormalViMode::commandRepeatLastChange() { const int repeatCount = getCount(); resetParser(); if (repeatCount > 1) { m_oneTimeCountOverride = repeatCount; } doc()->editStart(); m_viInputModeManager->repeatLastChange(); doc()->editEnd(); return true; } bool NormalViMode::commandAlignLine() { const int line = m_view->cursorPosition().line(); KTextEditor::Range alignRange(KTextEditor::Cursor(line, 0), KTextEditor::Cursor(line, 0)); doc()->align(m_view, alignRange); return true; } bool NormalViMode::commandAlignLines() { m_commandRange.normalize(); KTextEditor::Cursor start(m_commandRange.startLine, 0); KTextEditor::Cursor end(m_commandRange.endLine, 0); doc()->align(m_view, KTextEditor::Range(start, end)); return true; } bool NormalViMode::commandAddToNumber() { addToNumberUnderCursor(getCount()); return true; } bool NormalViMode::commandSubtractFromNumber() { addToNumberUnderCursor(-getCount()); return true; } bool NormalViMode::commandPrependToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); // move cursor to top left corner of selection m_commandRange.normalize(); c.setColumn(m_commandRange.startColumn); c.setLine(m_commandRange.startLine); updateCursor(c); m_stickyColumn = -1; m_viInputModeManager->getViInsertMode()->setBlockPrependMode(m_commandRange); return startInsertMode(); } bool NormalViMode::commandAppendToBlock() { KTextEditor::Cursor c(m_view->cursorPosition()); m_commandRange.normalize(); if (m_stickyColumn == (unsigned int)KateVi::EOL) { // append to EOL // move cursor to end of first line c.setLine(m_commandRange.startLine); c.setColumn(doc()->lineLength(c.line())); updateCursor(c); m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, AppendEOL); } else { m_viInputModeManager->getViInsertMode()->setBlockAppendMode(m_commandRange, Append); // move cursor to top right corner of selection c.setColumn(m_commandRange.endColumn + 1); c.setLine(m_commandRange.startLine); updateCursor(c); } m_stickyColumn = -1; return startInsertMode(); } bool NormalViMode::commandGoToNextJump() { KTextEditor::Cursor c = getNextJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandGoToPrevJump() { KTextEditor::Cursor c = getPrevJump(m_view->cursorPosition()); updateCursor(c); return true; } bool NormalViMode::commandSwitchToLeftView() { switchView(Left); return true; } bool NormalViMode::commandSwitchToDownView() { switchView(Down); return true; } bool NormalViMode::commandSwitchToUpView() { switchView(Up); return true; } bool NormalViMode::commandSwitchToRightView() { switchView(Right); return true; } bool NormalViMode::commandSwitchToNextView() { switchView(Next); return true; } bool NormalViMode::commandSplitHoriz() { return executeKateCommand(QStringLiteral("split")); } bool NormalViMode::commandSplitVert() { return executeKateCommand(QStringLiteral("vsplit")); } bool NormalViMode::commandCloseView() { return executeKateCommand(QStringLiteral("close")); } bool NormalViMode::commandSwitchToNextTab() { QString command = QStringLiteral("bn"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandSwitchToPrevTab() { QString command = QStringLiteral("bp"); if (m_iscounted) { command = command + QLatin1Char(' ') + QString::number(getCount()); } return executeKateCommand(command); } bool NormalViMode::commandFormatLine() { KTextEditor::Cursor c(m_view->cursorPosition()); reformatLines(c.line(), c.line() + getCount() - 1); return true; } bool NormalViMode::commandFormatLines() { reformatLines(m_commandRange.startLine, m_commandRange.endLine); return true; } bool NormalViMode::commandCollapseToplevelNodes() { #if 0 //FIXME FOLDING doc()->foldingTree()->collapseToplevelNodes(); #endif return true; } bool NormalViMode::commandStartRecordingMacro() { const QChar reg = m_keys[m_keys.size() - 1]; m_viInputModeManager->macroRecorder()->start(reg); return true; } bool NormalViMode::commandReplayMacro() { // "@" will have been added to the log; it needs to be cleared // *before* we replay the macro keypresses, else it can cause an infinite loop // if the macro contains a "." m_viInputModeManager->clearCurrentChangeLog(); const QChar reg = m_keys[m_keys.size() - 1]; const unsigned int count = getCount(); resetParser(); doc()->editBegin(); for (unsigned int i = 0; i < count; i++) { m_viInputModeManager->macroRecorder()->replay(reg); } doc()->editEnd(); return true; } bool NormalViMode::commandCloseNocheck() { return executeKateCommand(QStringLiteral("q!")); } bool NormalViMode::commandCloseWrite() { return executeKateCommand(QStringLiteral("wq")); } bool NormalViMode::commandCollapseLocal() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->collapseOne(c.line(), c.column()); #endif return true; } bool NormalViMode::commandExpandAll() { #if 0 //FIXME FOLDING doc()->foldingTree()->expandAll(); #endif return true; } bool NormalViMode::commandExpandLocal() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->expandOne(c.line() + 1, c.column()); #endif return true; } bool NormalViMode::commandToggleRegionVisibility() { #if 0 //FIXME FOLDING KTextEditor::Cursor c(m_view->cursorPosition()); doc()->foldingTree()->toggleRegionVisibility(c.line()); #endif return true; } //////////////////////////////////////////////////////////////////////////////// // MOTIONS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::motionDown() { return goLineDown(); } Range NormalViMode::motionUp() { return goLineUp(); } Range NormalViMode::motionLeft() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn -= getCount(); if (r.endColumn < 0) { r.endColumn = 0; } return r; } Range NormalViMode::motionRight() { KTextEditor::Cursor cursor(m_view->cursorPosition()); m_stickyColumn = -1; Range r(cursor, ExclusiveMotion); r.endColumn += getCount(); // make sure end position isn't > line length if (r.endColumn > doc()->lineLength(r.endLine)) { r.endColumn = doc()->lineLength(r.endLine); } return r; } Range NormalViMode::motionPageDown() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine += linesDisplayed(); if (r.endLine >= doc()->lines()) { r.endLine = doc()->lines() - 1; } return r; } Range NormalViMode::motionPageUp() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); r.endLine -= linesDisplayed(); if (r.endLine < 0) { r.endLine = 0; } return r; } Range NormalViMode::motionHalfPageDown() { if (commandScrollHalfPageDown()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionHalfPageUp() { if (commandScrollHalfPageUp()) { KTextEditor::Cursor c = m_view->cursorPosition(); m_commandRange.endLine = c.line(); m_commandRange.endColumn = c.column(); return m_commandRange; } return Range::invalid(); } Range NormalViMode::motionDownToFirstNonBlank() { Range r = goLineDown(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionUpToFirstNonBlank() { Range r = goLineUp(); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionWordForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; // Special case: If we're already on the very last character in the document, the motion should be // inclusive so the last character gets included if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { r.motionType = InclusiveMotion; } else { for (int i = 0; i < getCount(); i++) { c = findNextWordStart(c.line(), c.column()); // stop when at the last char in the document if (!c.isValid()) { c = doc()->documentEnd(); // if we still haven't "used up the count", make the motion inclusive, so that the last char // is included if (i < getCount()) { r.motionType = InclusiveMotion; } break; } } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWordBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDForward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findNextWORDStart(c.line(), c.column()); // stop when at the last char in the document if (c.line() == doc()->lines() - 1 && c.column() == doc()->lineLength(c.line()) - 1) { break; } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionWORDBackward() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, ExclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDStart(c.line(), c.column()); if (!c.isValid()) { c = KTextEditor::Cursor(0, 0); } } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWordEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findWORDEnd(c.line(), c.column()); } if (!c.isValid()) { c = doc()->documentEnd(); } r.endColumn = c.column(); r.endLine = c.line(); return r; } Range NormalViMode::motionToEndOfPrevWord() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWordEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEndOfPrevWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); Range r(c, InclusiveMotion); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { c = findPrevWORDEnd(c.line(), c.column()); if (c.isValid()) { r.endColumn = c.column(); r.endLine = c.line(); } else { r.endColumn = 0; r.endLine = 0; break; } } return r; } Range NormalViMode::motionToEOL() { KTextEditor::Cursor c(m_view->cursorPosition()); // set sticky column to a ridiculously high value so that the cursor will stick to EOL, // but only if it's a regular motion if (m_keys.size() == 1) { m_stickyColumn = KateVi::EOL; } unsigned int line = c.line() + (getCount() - 1); Range r(line, doc()->lineLength(line) - 1, InclusiveMotion); return r; } Range NormalViMode::motionToColumn0() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); Range r(cursor.line(), 0, ExclusiveMotion); return r; } Range NormalViMode::motionToFirstCharacterOfLine() { m_stickyColumn = -1; KTextEditor::Cursor cursor(m_view->cursorPosition()); int c = getFirstNonBlank(); Range r(cursor.line(), c, ExclusiveMotion); return r; } Range NormalViMode::motionFindChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = cursor.column(); for (int i = 0; i < getCount(); i++) { matchColumn = line.indexOf(m_keys.rightRef(1), matchColumn + 1); if (matchColumn == -1) { break; } } Range r; if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionFindCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; int matchColumn = -1; int hits = 0; int i = cursor.column() - 1; while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } Range r(cursor, ExclusiveMotion); if (matchColumn != -1) { r.endColumn = matchColumn; r.endLine = cursor.line(); } else { return Range::invalid(); } return r; } Range NormalViMode::motionToChar() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); m_stickyColumn = -1; Range r; r.endColumn = -1; r.endLine = -1; int matchColumn = cursor.column() + (m_isRepeatedTFcommand ? 2 : 1); for (int i = 0; i < getCount(); i++) { const int lastColumn = matchColumn; matchColumn = line.indexOf(m_keys.right(1), matchColumn + ((i > 0) ? 1 : 0)); if (matchColumn == -1) { if (m_isRepeatedTFcommand) { matchColumn = lastColumn; } else { return Range::invalid(); } break; } } r.endColumn = matchColumn - 1; r.endLine = cursor.line(); m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionToCharBackward() { m_lastTFcommand = m_keys; KTextEditor::Cursor cursor(m_view->cursorPosition()); QString line = getLine(); const int originalColumn = cursor.column(); m_stickyColumn = -1; int matchColumn = originalColumn - 1; int hits = 0; int i = cursor.column() - (m_isRepeatedTFcommand ? 2 : 1); Range r(cursor, ExclusiveMotion); while (hits != getCount() && i >= 0) { if (line.at(i) == m_keys.at(m_keys.size() - 1)) { hits++; } if (hits == getCount()) { matchColumn = i; } i--; } if (hits == getCount()) { r.endColumn = matchColumn + 1; r.endLine = cursor.line(); } else { r.valid = false; } m_isRepeatedTFcommand = false; return r; } Range NormalViMode::motionRepeatlastTF() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToChar(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToCharBackward(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionRepeatlastTFBackward() { if (!m_lastTFcommand.isEmpty()) { m_isRepeatedTFcommand = true; m_keys = m_lastTFcommand; if (m_keys.at(0) == QLatin1Char('f')) { return motionFindCharBackward(); } else if (m_keys.at(0) == QLatin1Char('F')) { return motionFindChar(); } else if (m_keys.at(0) == QLatin1Char('t')) { return motionToCharBackward(); } else if (m_keys.at(0) == QLatin1Char('T')) { return motionToChar(); } } // there was no previous t/f command return Range::invalid(); } Range NormalViMode::motionToLineFirst() { Range r(getCount() - 1, 0, InclusiveMotion); m_stickyColumn = -1; if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToLineLast() { Range r(doc()->lines() - 1, 0, InclusiveMotion); m_stickyColumn = -1; // don't use getCount() here, no count and a count of 1 is different here... if (m_count != 0) { r.endLine = m_count - 1; } if (r.endLine > doc()->lines() - 1) { r.endLine = doc()->lines() - 1; } r.jump = true; return r; } Range NormalViMode::motionToScreenColumn() { m_stickyColumn = -1; KTextEditor::Cursor c(m_view->cursorPosition()); int column = getCount() - 1; if (doc()->lineLength(c.line()) - 1 < (int)getCount() - 1) { column = doc()->lineLength(c.line()) - 1; } return Range(c.line(), column, ExclusiveMotion); } Range NormalViMode::motionToMark() { Range r; m_stickyColumn = -1; QChar reg = m_keys.at(m_keys.size() - 1); KTextEditor::Cursor c = m_viInputModeManager->marks()->getMarkPosition(reg); if (c.isValid()) { r.endLine = c.line(); r.endColumn = c.column(); } else { error(i18n("Mark not set: %1", m_keys.right(1))); r.valid = false; } r.jump = true; return r; } Range NormalViMode::motionToMarkLine() { Range r = motionToMark(); r.endColumn = getFirstNonBlank(r.endLine); r.jump = true; m_stickyColumn = -1; return r; } Range NormalViMode::motionToMatchingItem() { Range r; int lines = doc()->lines(); // If counted, then it's not a motion to matching item anymore, // but a motion to the N'th percentage of the document if (isCounted()) { int count = getCount(); if (count > 100) { return r; } r.endLine = qRound(lines * count / 100.0) - 1; r.endColumn = 0; return r; } KTextEditor::Cursor c(m_view->cursorPosition()); QString l = getLine(); int n1 = l.indexOf(m_matchItemRegex, c.column()); m_stickyColumn = -1; if (n1 < 0) { return Range::invalid(); } QRegExp brackets(QLatin1String("[(){}\\[\\]]")); // use Kate's built-in matching bracket finder for brackets if (brackets.indexIn(l, n1) == n1) { // findMatchingBracket requires us to move the cursor to the // first bracket, but we don't want the cursor to really move // in case this is e.g. a yank, so restore it to its original // position afterwards. c.setColumn(n1 + 1); const KTextEditor::Cursor oldCursorPos = m_view->cursorPosition(); updateCursor(c); // find the matching one c = m_viewInternal->findMatchingBracket(); if (c > m_view->cursorPosition()) { c.setColumn(c.column() - 1); } m_view->setCursorPosition(oldCursorPos); } else { // text item we want to find a matching item for int n2 = l.indexOf(QRegExp(QLatin1String("\\b|\\s|$")), n1); QString item = l.mid(n1, n2 - n1); QString matchingItem = m_matchingItems[ item ]; int toFind = 1; int line = c.line(); int column = n2 - item.length(); bool reverse = false; if (matchingItem.startsWith(QLatin1Char('-'))) { matchingItem.remove(0, 1); // remove the '-' reverse = true; } // make sure we don't hit the text item we started the search from if (column == 0 && reverse) { column -= item.length(); } int itemIdx; int matchItemIdx; while (toFind > 0) { if (reverse) { itemIdx = l.lastIndexOf(item, column - 1); matchItemIdx = l.lastIndexOf(matchingItem, column - 1); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx > matchItemIdx)) { ++toFind; } } else { itemIdx = l.indexOf(item, column); matchItemIdx = l.indexOf(matchingItem, column); if (itemIdx != -1 && (matchItemIdx == -1 || itemIdx < matchItemIdx)) { ++toFind; } } if (matchItemIdx != -1 || itemIdx != -1) { if (!reverse) { column = qMin((unsigned int)itemIdx, (unsigned int)matchItemIdx); } else { column = qMax(itemIdx, matchItemIdx); } } if (matchItemIdx != -1) { // match on current line if (matchItemIdx == column) { --toFind; c.setLine(line); c.setColumn(column); } } else { // no match, advance one line if possible (reverse) ? --line : ++line; column = 0; if ((!reverse && line >= lines) || (reverse && line < 0)) { r.valid = false; break; } else { l = getLine(line); } } } } r.endLine = c.line(); r.endColumn = c.column(); r.jump = true; return r; } Range NormalViMode::motionToNextBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '{' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockStart() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('{'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // With a command, do not include the { or the cursor position. r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount()); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { // Delete from cursor (inclusive) to the '}' (exclusive). // If we are on the first column, then delete the entire current line. r.motionType = ExclusiveMotion; if (m_view->cursorPosition().column() != 0) { r.endLine--; r.endColumn = doc()->lineLength(r.endLine); } } return r; } Range NormalViMode::motionToPreviousBraceBlockEnd() { Range r; m_stickyColumn = -1; int line = findLineStartingWitchChar(QLatin1Char('}'), getCount(), false); if (line == -1) { return Range::invalid(); } r.endLine = line; r.endColumn = 0; r.jump = true; if (motionWillBeUsedWithCommand()) { r.motionType = ExclusiveMotion; } return r; } Range NormalViMode::motionToNextOccurrence() { const QString word = getWordUnderCursor(); const Range match = m_viInputModeManager->searcher()->findWordForMotion(word, false, getWordRangeUnderCursor().start(), getCount()); return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToPrevOccurrence() { const QString word = getWordUnderCursor(); const Range match = m_viInputModeManager->searcher()->findWordForMotion(word, true, getWordRangeUnderCursor().start(), getCount()); return Range(match.startLine, match.startColumn, ExclusiveMotion); } Range NormalViMode::motionToFirstLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int) m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() - m_view->cursorPosition().line() + 1; } else { lines_to_go = - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToMiddleLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int) m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - linesDisplayed() / 2 - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() / 2 - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToLastLineOfWindow() { int lines_to_go; if (linesDisplayed() <= (unsigned int) m_viewInternal->endLine()) { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } else { lines_to_go = m_viewInternal->endLine() - m_view->cursorPosition().line(); } Range r = goLineUpDown(lines_to_go); r.endColumn = getFirstNonBlank(r.endLine); return r; } Range NormalViMode::motionToNextVisualLine() { return goVisualLineUpDown(getCount()); } Range NormalViMode::motionToPrevVisualLine() { return goVisualLineUpDown(-getCount()); } Range NormalViMode::motionToPreviousSentence() { KTextEditor::Cursor c = findSentenceStart(); int linenum = c.line(), column; const bool skipSpaces = doc()->line(linenum).isEmpty(); if (skipSpaces) { linenum--; if (linenum >= 0) { column = doc()->line(linenum).size() - 1; } } else { column = c.column(); } for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } if (column < 0 && !line.isEmpty()) { column = line.size() - 1; } for (int j = column; j >= 0; j--) { if (skipSpaces || QStringLiteral(".?!").indexOf(line.at(j)) != -1) { c.setLine(i); c.setColumn(j); updateCursor(c); c = findSentenceStart(); return Range(c, InclusiveMotion); } } column = line.size() - 1; } return Range(0, 0, InclusiveMotion); } Range NormalViMode::motionToNextSentence() { KTextEditor::Cursor c = findSentenceEnd(); int linenum = c.line(), column = c.column() + 1; const bool skipSpaces = doc()->line(linenum).isEmpty(); for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); if (line.isEmpty() && !skipSpaces) { return Range(i, 0, InclusiveMotion); } for (int j = column; j < line.size(); j++) { if (!line.at(j).isSpace()) { return Range(i, j, InclusiveMotion); } } column = 0; } c = doc()->documentEnd(); return Range(c, InclusiveMotion); } Range NormalViMode::motionToBeforeParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line--; } while (line >= 0 && getLine(line + 1).length() == 0); while (line > 0 && getLine(line).length() != 0) { line--; } } if (line < 0) { line = 0; } Range r(line, 0, InclusiveMotion); return r; } Range NormalViMode::motionToAfterParagraph() { KTextEditor::Cursor c(m_view->cursorPosition()); int line = c.line(); m_stickyColumn = -1; for (int i = 0; i < getCount(); i++) { // advance at least one line, but if there are consecutive blank lines // skip them all do { line++; } while (line <= doc()->lines() - 1 && getLine(line - 1).length() == 0); while (line < doc()->lines() - 1 && getLine(line).length() != 0) { line++; } } if (line >= doc()->lines()) { line = doc()->lines() - 1; } // if we ended up on the last line, the cursor should be placed on the last column int column = (line == doc()->lines() - 1) ? qMax(getLine(line).length() - 1, 0) : 0; return Range(line, column, InclusiveMotion); } Range NormalViMode::motionToIncrementalSearchMatch() { return Range(m_positionWhenIncrementalSearchBegan.line(), m_positionWhenIncrementalSearchBegan.column(), m_view->cursorPosition().line(), m_view->cursorPosition().column(), ExclusiveMotion); } //////////////////////////////////////////////////////////////////////////////// // TEXT OBJECTS //////////////////////////////////////////////////////////////////////////////// Range NormalViMode::textObjectAWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWordEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWord() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWordStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } // need to start search in column-1 because it might be a one-character word KTextEditor::Cursor c2(c.line(), c.column() - 1); for (int i = 0; i < getCount(); i++) { c2 = findWordEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } Range NormalViMode::textObjectAWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = c; bool startedOnSpace = false; if (doc()->characterAt(c).isSpace()) { startedOnSpace = true; } else { c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } } KTextEditor::Cursor c2 = KTextEditor::Cursor(c.line(), c.column() - 1); for (int i = 1; i <= getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column()); } if (!c1.isValid() || !c2.isValid()) { return Range::invalid(); } // Adhere to some of Vim's bizarre rules of whether to swallow ensuing spaces or not. // Don't ask ;) const KTextEditor::Cursor nextWordStart = findNextWordStart(c2.line(), c2.column()); if (nextWordStart.isValid() && nextWordStart.line() == c2.line()) { if (!startedOnSpace) { c2 = KTextEditor::Cursor(nextWordStart.line(), nextWordStart.column() - 1); } } else { c2 = KTextEditor::Cursor(c2.line(), doc()->lineLength(c2.line()) - 1); } bool swallowCarriageReturnAtEndOfLine = false; if (c2.line() != c.line() && c2.column() == doc()->lineLength(c2.line()) - 1) { // Greedily descend to the next line, so as to swallow the carriage return on this line. c2 = KTextEditor::Cursor(c2.line() + 1, 0); swallowCarriageReturnAtEndOfLine = true; } const bool swallowPrecedingSpaces = (c2.column() == doc()->lineLength(c2.line()) - 1 && !doc()->characterAt(c2).isSpace()) || startedOnSpace || swallowCarriageReturnAtEndOfLine; if (swallowPrecedingSpaces) { if (c1.column() != 0) { const KTextEditor::Cursor previousNonSpace = findPrevWORDEnd(c.line(), c.column()); if (previousNonSpace.isValid() && previousNonSpace.line() == c1.line()) { c1 = KTextEditor::Cursor(previousNonSpace.line(), previousNonSpace.column() + 1); } else if (startedOnSpace || swallowCarriageReturnAtEndOfLine) { c1 = KTextEditor::Cursor(c1.line(), 0); } } } return Range(c1, c2, !swallowCarriageReturnAtEndOfLine ? InclusiveMotion : ExclusiveMotion); } Range NormalViMode::textObjectInnerWORD() { KTextEditor::Cursor c(m_view->cursorPosition()); KTextEditor::Cursor c1 = findPrevWORDStart(c.line(), c.column() + 1, true); if (!c1.isValid()) { c1 = KTextEditor::Cursor(0, 0); } KTextEditor::Cursor c2(c); for (int i = 0; i < getCount(); i++) { c2 = findWORDEnd(c2.line(), c2.column(), true); } if (!c2.isValid()) { c2 = doc()->documentEnd(); } // sanity check if (c1.line() != c2.line() || c1.column() > c2.column()) { return Range::invalid(); } return Range(c1, c2, InclusiveMotion); } KTextEditor::Cursor NormalViMode::findSentenceStart() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(), column = c.column(); int prev = column; for (int i = linenum; i >= 0; i--) { const QString &line = doc()->line(i); if (i != linenum) { column = line.size() - 1; } // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor((i != linenum) ? i + 1 : i, prev); } prev = column; for (int j = column; j >= 0; j--) { if (line.at(j).isSpace()) { int lastSpace = j--; for (; j >= 0 && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j--); if (j >= 0 && QStringLiteral(".!?").indexOf(line.at(j)) != -1) { return KTextEditor::Cursor(i, prev); } j = lastSpace; } else prev = j; } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findSentenceEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int linenum = c.line(), column = c.column(); int j = 0, prev = 0; for (int i = linenum; i < doc()->lines(); i++) { const QString &line = doc()->line(i); // An empty line is the end of a paragraph. if (line.isEmpty()) { return KTextEditor::Cursor(linenum, j); } // Iterating over the line to reach any '.', '!', '?' for (j = column; j < line.size(); j++) { if (QStringLiteral(".!?").indexOf(line.at(j)) != -1) { prev = j++; // Skip possible closing characters. - for (; j < line.size() && QString::fromLatin1("\"')]").indexOf(line.at(j)) != -1; j++); + for (; j < line.size() && QStringLiteral("\"')]").indexOf(line.at(j)) != -1; j++); if (j >= line.size()) { return KTextEditor::Cursor(i, j - 1); } // And hopefully we're done... if (line.at(j).isSpace()) { return KTextEditor::Cursor(i, j - 1); } j = prev; } } linenum = i; prev = column; column = 0; } return KTextEditor::Cursor(linenum, j - 1); } KTextEditor::Cursor NormalViMode::findParagraphStart() { KTextEditor::Cursor c(m_view->cursorPosition()); const bool firstBlank = doc()->line(c.line()).isEmpty(); int prev = c.line(); for (int i = prev; i >= 0; i--) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i + 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i--; for (; i >= 0 && doc()->line(i).isEmpty(); i--, prev--); } return KTextEditor::Cursor(prev, 0); } } return KTextEditor::Cursor(0, 0); } KTextEditor::Cursor NormalViMode::findParagraphEnd() { KTextEditor::Cursor c(m_view->cursorPosition()); int prev = c.line(), lines = doc()->lines(); const bool firstBlank = doc()->line(prev).isEmpty(); for (int i = prev; i < lines; i++) { if (doc()->line(i).isEmpty()) { if (i != prev) { prev = i - 1; } /* Skip consecutive empty lines. */ if (firstBlank) { i++; for (; i < lines && doc()->line(i).isEmpty(); i++, prev++); } int length = doc()->lineLength(prev); return KTextEditor::Cursor(prev, (length <= 0) ? 0 : length - 1); } } return doc()->documentEnd(); } Range NormalViMode::textObjectInnerSentence() { Range r; KTextEditor::Cursor c1 = findSentenceStart(); KTextEditor::Cursor c2 = findSentenceEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectASentence() { int i; Range r = textObjectInnerSentence(); const QString &line = doc()->line(r.endLine); // Skip whitespaces and tabs. for (i = r.endColumn + 1; i < line.size(); i++) { if (!line.at(i).isSpace()) { break; } } r.endColumn = i - 1; // Remove preceding spaces. if (r.startColumn != 0) { if (r.endColumn == line.size() - 1 && !line.at(r.endColumn).isSpace()) { const QString &line = doc()->line(r.startLine); for (i = r.startColumn - 1; i >= 0; i--) { if (!line.at(i).isSpace()) { break; } } r.startColumn = i + 1; } } return r; } Range NormalViMode::textObjectInnerParagraph() { Range r; KTextEditor::Cursor c1 = findParagraphStart(); KTextEditor::Cursor c2 = findParagraphEnd(); updateCursor(c1); r.startLine = c1.line(); r.startColumn = c1.column(); r.endLine = c2.line(); r.endColumn = c2.column(); return r; } Range NormalViMode::textObjectAParagraph() { Range r = textObjectInnerParagraph(); int lines = doc()->lines(); if (r.endLine + 1 < lines) { // If the next line is empty, remove all subsequent empty lines. // Otherwise we'll grab the next paragraph. if (doc()->line(r.endLine + 1).isEmpty()) { for (int i = r.endLine + 1; i < lines && doc()->line(i).isEmpty(); i++) { r.endLine++; } r.endColumn = 0; } else { KTextEditor::Cursor prev = m_view->cursorPosition(); KTextEditor::Cursor c(r.endLine + 1, 0); updateCursor(c); c = findParagraphEnd(); updateCursor(prev); r.endLine = c.line(); r.endColumn = c.column(); } } else if (doc()->lineLength(r.startLine) > 0) { // We went too far, but maybe we can grab previous empty lines. for (int i = r.startLine - 1; i >= 0 && doc()->line(i).isEmpty(); i--) { r.startLine--; } r.startColumn = 0; updateCursor(KTextEditor::Cursor(r.startLine, r.startColumn)); } else { // We went too far and we're on empty lines, do nothing. return Range::invalid(); } return r; } Range NormalViMode::textObjectAQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), false); } Range NormalViMode::textObjectInnerQuoteDouble() { return findSurroundingQuotes(QLatin1Char('"'), true); } Range NormalViMode::textObjectAQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), false); } Range NormalViMode::textObjectInnerQuoteSingle() { return findSurroundingQuotes(QLatin1Char('\''), true); } Range NormalViMode::textObjectABackQuote() { return findSurroundingQuotes(QLatin1Char('`'), false); } Range NormalViMode::textObjectInnerBackQuote() { return findSurroundingQuotes(QLatin1Char('`'), true); } Range NormalViMode::textObjectAParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), false, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectInnerParen() { return findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), true, QLatin1Char('('), QLatin1Char(')')); } Range NormalViMode::textObjectABracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), false, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectInnerBracket() { return findSurroundingBrackets(QLatin1Char('['), QLatin1Char(']'), true, QLatin1Char('['), QLatin1Char(']')); } Range NormalViMode::textObjectACurlyBracket() { return findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), false, QLatin1Char('{'), QLatin1Char('}')); } Range NormalViMode::textObjectInnerCurlyBracket() { const Range allBetweenCurlyBrackets = findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), true, QLatin1Char('{'), QLatin1Char('}')); // Emulate the behaviour of vim, which tries to leave the closing bracket on its own line // if it was originally on a line different to that of the opening bracket. Range innerCurlyBracket(allBetweenCurlyBrackets); if (innerCurlyBracket.startLine != innerCurlyBracket.endLine) { const bool openingBraceIsLastCharOnLine = innerCurlyBracket.startColumn == doc()->line(innerCurlyBracket.startLine).length(); const bool stuffToDeleteIsAllOnEndLine = openingBraceIsLastCharOnLine && innerCurlyBracket.endLine == innerCurlyBracket.startLine + 1; const QString textLeadingClosingBracket = doc()->line(innerCurlyBracket.endLine).mid(0, innerCurlyBracket.endColumn + 1); const bool closingBracketHasLeadingNonWhitespace = !textLeadingClosingBracket.trimmed().isEmpty(); if (stuffToDeleteIsAllOnEndLine) { if (!closingBracketHasLeadingNonWhitespace) { // Nothing there to select - abort. return Range::invalid(); } else { // Shift the beginning of the range to the start of the line containing the closing bracket. innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; } } else { if (openingBraceIsLastCharOnLine && !closingBracketHasLeadingNonWhitespace) { innerCurlyBracket.startLine++; innerCurlyBracket.startColumn = 0; m_lastMotionWasLinewiseInnerBlock = true; } { // The line containing the end bracket is left alone if the end bracket is preceded by just whitespace, // else we need to delete everything (i.e. end up with "{}") if (!closingBracketHasLeadingNonWhitespace) { // Shrink the endpoint of the range so that it ends at the end of the line above, // leaving the closing bracket on its own line. innerCurlyBracket.endLine--; innerCurlyBracket.endColumn = doc()->line(innerCurlyBracket.endLine).length(); } } } } return innerCurlyBracket; } Range NormalViMode::textObjectAInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), false, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectInnerInequalitySign() { return findSurroundingBrackets(QLatin1Char('<'), QLatin1Char('>'), true, QLatin1Char('<'), QLatin1Char('>')); } Range NormalViMode::textObjectAComma() { return textObjectComma(false); } Range NormalViMode::textObjectInnerComma() { return textObjectComma(true); } // add commands // when adding commands here, remember to add them to visual mode too (if applicable) void NormalViMode::initializeCommands() { ADDCMD("a", commandEnterInsertModeAppend, IS_CHANGE); ADDCMD("A", commandEnterInsertModeAppendEOL, IS_CHANGE); ADDCMD("i", commandEnterInsertMode, IS_CHANGE); ADDCMD("", commandEnterInsertMode, IS_CHANGE); ADDCMD("I", commandEnterInsertModeBeforeFirstNonBlankInLine, IS_CHANGE); ADDCMD("gi", commandEnterInsertModeLast, IS_CHANGE); ADDCMD("v", commandEnterVisualMode, 0); ADDCMD("V", commandEnterVisualLineMode, 0); ADDCMD("", commandEnterVisualBlockMode, 0); ADDCMD("gv", commandReselectVisual, SHOULD_NOT_RESET); ADDCMD("o", commandOpenNewLineUnder, IS_CHANGE); ADDCMD("O", commandOpenNewLineOver, IS_CHANGE); ADDCMD("J", commandJoinLines, IS_CHANGE); ADDCMD("c", commandChange, IS_CHANGE | NEEDS_MOTION); ADDCMD("C", commandChangeToEOL, IS_CHANGE); ADDCMD("cc", commandChangeLine, IS_CHANGE); ADDCMD("s", commandSubstituteChar, IS_CHANGE); ADDCMD("S", commandSubstituteLine, IS_CHANGE); ADDCMD("dd", commandDeleteLine, IS_CHANGE); ADDCMD("d", commandDelete, IS_CHANGE | NEEDS_MOTION); ADDCMD("D", commandDeleteToEOL, IS_CHANGE); ADDCMD("x", commandDeleteChar, IS_CHANGE); ADDCMD("", commandDeleteChar, IS_CHANGE); ADDCMD("X", commandDeleteCharBackward, IS_CHANGE); ADDCMD("gu", commandMakeLowercase, IS_CHANGE | NEEDS_MOTION); ADDCMD("guu", commandMakeLowercaseLine, IS_CHANGE); ADDCMD("gU", commandMakeUppercase, IS_CHANGE | NEEDS_MOTION); ADDCMD("gUU", commandMakeUppercaseLine, IS_CHANGE); ADDCMD("y", commandYank, NEEDS_MOTION); ADDCMD("yy", commandYankLine, 0); ADDCMD("Y", commandYankToEOL, 0); ADDCMD("p", commandPaste, IS_CHANGE); ADDCMD("P", commandPasteBefore, IS_CHANGE); ADDCMD("gp", commandgPaste, IS_CHANGE); ADDCMD("gP", commandgPasteBefore, IS_CHANGE); ADDCMD("]p", commandIndentedPaste, IS_CHANGE); ADDCMD("[p", commandIndentedPasteBefore, IS_CHANGE); ADDCMD("r.", commandReplaceCharacter, IS_CHANGE | REGEX_PATTERN); ADDCMD("R", commandEnterReplaceMode, IS_CHANGE); ADDCMD(":", commandSwitchToCmdLine, 0); ADDCMD("u", commandUndo, 0); ADDCMD("", commandRedo, 0); ADDCMD("U", commandRedo, 0); ADDCMD("m.", commandSetMark, REGEX_PATTERN); ADDCMD(">>", commandIndentLine, IS_CHANGE); ADDCMD("<<", commandUnindentLine, IS_CHANGE); ADDCMD(">", commandIndentLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("<", commandUnindentLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("", commandScrollPageDown, 0); ADDCMD("", commandScrollPageDown, 0); ADDCMD("", commandScrollPageUp, 0); ADDCMD("", commandScrollPageUp, 0); ADDCMD("", commandScrollHalfPageUp, 0); ADDCMD("", commandScrollHalfPageDown, 0); ADDCMD("z.", commandCenterViewOnNonBlank, 0); ADDCMD("zz", commandCenterViewOnCursor, 0); ADDCMD("z", commandTopViewOnNonBlank, 0); ADDCMD("zt", commandTopViewOnCursor, 0); ADDCMD("z-", commandBottomViewOnNonBlank, 0); ADDCMD("zb", commandBottomViewOnCursor, 0); ADDCMD("ga", commandPrintCharacterCode, SHOULD_NOT_RESET); ADDCMD(".", commandRepeatLastChange, 0); ADDCMD("==", commandAlignLine, IS_CHANGE); ADDCMD("=", commandAlignLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("~", commandChangeCase, IS_CHANGE); ADDCMD("g~", commandChangeCaseRange, IS_CHANGE | NEEDS_MOTION); ADDCMD("g~~", commandChangeCaseLine, IS_CHANGE); ADDCMD("", commandAddToNumber, IS_CHANGE); ADDCMD("", commandSubtractFromNumber, IS_CHANGE); ADDCMD("", commandGoToPrevJump, 0); ADDCMD("", commandGoToNextJump, 0); ADDCMD("h", commandSwitchToLeftView, 0); ADDCMD("", commandSwitchToLeftView, 0); ADDCMD("", commandSwitchToLeftView, 0); ADDCMD("j", commandSwitchToDownView, 0); ADDCMD("", commandSwitchToDownView, 0); ADDCMD("", commandSwitchToDownView, 0); ADDCMD("k", commandSwitchToUpView, 0); ADDCMD("", commandSwitchToUpView, 0); ADDCMD("", commandSwitchToUpView, 0); ADDCMD("l", commandSwitchToRightView, 0); ADDCMD("", commandSwitchToRightView, 0); ADDCMD("", commandSwitchToRightView, 0); ADDCMD("w", commandSwitchToNextView, 0); ADDCMD("", commandSwitchToNextView, 0); ADDCMD("s", commandSplitHoriz, 0); ADDCMD("S", commandSplitHoriz, 0); ADDCMD("", commandSplitHoriz, 0); ADDCMD("v", commandSplitVert, 0); ADDCMD("", commandSplitVert, 0); ADDCMD("c", commandCloseView, 0); ADDCMD("gt", commandSwitchToNextTab, 0); ADDCMD("gT", commandSwitchToPrevTab, 0); ADDCMD("gqq", commandFormatLine, IS_CHANGE); ADDCMD("gq", commandFormatLines, IS_CHANGE | NEEDS_MOTION); ADDCMD("zo", commandExpandLocal, 0); ADDCMD("zc", commandCollapseLocal, 0); ADDCMD("za", commandToggleRegionVisibility, 0); ADDCMD("zr", commandExpandAll, 0); ADDCMD("zm", commandCollapseToplevelNodes, 0); ADDCMD("q.", commandStartRecordingMacro, REGEX_PATTERN); ADDCMD("@.", commandReplayMacro, REGEX_PATTERN); ADDCMD("ZZ", commandCloseWrite, 0); ADDCMD("ZQ", commandCloseNocheck, 0); // regular motions ADDMOTION("h", motionLeft, 0); ADDMOTION("", motionLeft, 0); ADDMOTION("", motionLeft, 0); ADDMOTION("j", motionDown, 0); ADDMOTION("", motionDown, 0); ADDMOTION("", motionDownToFirstNonBlank, 0); ADDMOTION("", motionDownToFirstNonBlank, 0); ADDMOTION("k", motionUp, 0); ADDMOTION("", motionUp, 0); ADDMOTION("-", motionUpToFirstNonBlank, 0); ADDMOTION("l", motionRight, 0); ADDMOTION("", motionRight, 0); ADDMOTION(" ", motionRight, 0); ADDMOTION("$", motionToEOL, 0); ADDMOTION("", motionToEOL, 0); ADDMOTION("0", motionToColumn0, 0); ADDMOTION("", motionToColumn0, 0); ADDMOTION("^", motionToFirstCharacterOfLine, 0); ADDMOTION("f.", motionFindChar, REGEX_PATTERN); ADDMOTION("F.", motionFindCharBackward, REGEX_PATTERN); ADDMOTION("t.", motionToChar, REGEX_PATTERN); ADDMOTION("T.", motionToCharBackward, REGEX_PATTERN); ADDMOTION(";", motionRepeatlastTF, 0); ADDMOTION(",", motionRepeatlastTFBackward, 0); ADDMOTION("n", motionFindNext, 0); ADDMOTION("N", motionFindPrev, 0); ADDMOTION("gg", motionToLineFirst, 0); ADDMOTION("G", motionToLineLast, 0); ADDMOTION("w", motionWordForward, IS_NOT_LINEWISE); ADDMOTION("W", motionWORDForward, IS_NOT_LINEWISE); ADDMOTION("", motionWordForward, IS_NOT_LINEWISE); ADDMOTION("", motionWordBackward, IS_NOT_LINEWISE); ADDMOTION("b", motionWordBackward, 0); ADDMOTION("B", motionWORDBackward, 0); ADDMOTION("e", motionToEndOfWord, 0); ADDMOTION("E", motionToEndOfWORD, 0); ADDMOTION("ge", motionToEndOfPrevWord, 0); ADDMOTION("gE", motionToEndOfPrevWORD, 0); ADDMOTION("|", motionToScreenColumn, 0); ADDMOTION("%", motionToMatchingItem, IS_NOT_LINEWISE); ADDMOTION("`[a-zA-Z^><\\.\\[\\]]", motionToMark, REGEX_PATTERN); ADDMOTION("'[a-zA-Z^><]", motionToMarkLine, REGEX_PATTERN); ADDMOTION("[[", motionToPreviousBraceBlockStart, IS_NOT_LINEWISE); ADDMOTION("]]", motionToNextBraceBlockStart, IS_NOT_LINEWISE); ADDMOTION("[]", motionToPreviousBraceBlockEnd, IS_NOT_LINEWISE); ADDMOTION("][", motionToNextBraceBlockEnd, IS_NOT_LINEWISE); ADDMOTION("*", motionToNextOccurrence, 0); ADDMOTION("#", motionToPrevOccurrence, 0); ADDMOTION("H", motionToFirstLineOfWindow, 0); ADDMOTION("M", motionToMiddleLineOfWindow, 0); ADDMOTION("L", motionToLastLineOfWindow, 0); ADDMOTION("gj", motionToNextVisualLine, 0); ADDMOTION("gk", motionToPrevVisualLine, 0); ADDMOTION("(", motionToPreviousSentence, 0 ); ADDMOTION(")", motionToNextSentence, 0 ); ADDMOTION("{", motionToBeforeParagraph, 0); ADDMOTION("}", motionToAfterParagraph, 0); // text objects ADDMOTION("iw", textObjectInnerWord, 0); ADDMOTION("aw", textObjectAWord, IS_NOT_LINEWISE); ADDMOTION("iW", textObjectInnerWORD, 0); ADDMOTION("aW", textObjectAWORD, IS_NOT_LINEWISE); ADDMOTION("is", textObjectInnerSentence, IS_NOT_LINEWISE ); ADDMOTION("as", textObjectASentence, IS_NOT_LINEWISE ); ADDMOTION("ip", textObjectInnerParagraph, IS_NOT_LINEWISE ); ADDMOTION("ap", textObjectAParagraph, IS_NOT_LINEWISE ); ADDMOTION("i\"", textObjectInnerQuoteDouble, IS_NOT_LINEWISE); ADDMOTION("a\"", textObjectAQuoteDouble, IS_NOT_LINEWISE); ADDMOTION("i'", textObjectInnerQuoteSingle, IS_NOT_LINEWISE); ADDMOTION("a'", textObjectAQuoteSingle, IS_NOT_LINEWISE); ADDMOTION("i`", textObjectInnerBackQuote, IS_NOT_LINEWISE); ADDMOTION("a`", textObjectABackQuote, IS_NOT_LINEWISE); ADDMOTION("i[()b]", textObjectInnerParen, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[()b]", textObjectAParen, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[{}B]", textObjectInnerCurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[{}B]", textObjectACurlyBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[><]", textObjectInnerInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[><]", textObjectAInequalitySign, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i[\\[\\]]", textObjectInnerBracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("a[\\[\\]]", textObjectABracket, REGEX_PATTERN | IS_NOT_LINEWISE); ADDMOTION("i,", textObjectInnerComma, IS_NOT_LINEWISE); ADDMOTION("a,", textObjectAComma, IS_NOT_LINEWISE); ADDMOTION("/", motionToIncrementalSearchMatch, IS_NOT_LINEWISE); ADDMOTION("?", motionToIncrementalSearchMatch, IS_NOT_LINEWISE); } QRegExp NormalViMode::generateMatchingItemRegex() const { QString pattern(QStringLiteral("\\[|\\]|\\{|\\}|\\(|\\)|")); QList keys = m_matchingItems.keys(); for (int i = 0; i < keys.size(); i++) { QString s = m_matchingItems[ keys[ i ] ]; s.replace(QRegExp(QLatin1String("^-")), QChar()); s.replace(QRegExp(QLatin1String("\\*")), QStringLiteral("\\*")); s.replace(QRegExp(QLatin1String("\\+")), QStringLiteral("\\+")); s.replace(QRegExp(QLatin1String("\\[")), QStringLiteral("\\[")); s.replace(QRegExp(QLatin1String("\\]")), QStringLiteral("\\]")); s.replace(QRegExp(QLatin1String("\\(")), QStringLiteral("\\(")); s.replace(QRegExp(QLatin1String("\\)")), QStringLiteral("\\)")); s.replace(QRegExp(QLatin1String("\\{")), QStringLiteral("\\{")); s.replace(QRegExp(QLatin1String("\\}")), QStringLiteral("\\}")); pattern.append(s); if (i != keys.size() - 1) { pattern.append(QLatin1Char('|')); } } return QRegExp(pattern); } // returns the operation mode that should be used. this is decided by using the following heuristic: // 1. if we're in visual block mode, it should be Block // 2. if we're in visual line mode OR the range spans several lines, it should be LineWise // 3. if neither of these is true, CharWise is returned // 4. there are some motion that makes all operator charwise, if we have one of them mode will be CharWise OperationMode NormalViMode::getOperationMode() const { OperationMode m = CharWise; if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualBlockMode) { m = Block; } else if (m_viInputModeManager->getCurrentViMode() == ViMode::VisualLineMode || (m_commandRange.startLine != m_commandRange.endLine && m_viInputModeManager->getCurrentViMode() != ViMode::VisualMode)) { m = LineWise; } if (m_commandWithMotion && !m_linewiseCommand) { m = CharWise; } if (m_lastMotionWasLinewiseInnerBlock) { m = LineWise; } return m; } bool NormalViMode::paste(PasteLocation pasteLocation, bool isgPaste, bool isIndentedPaste) { KTextEditor::Cursor pasteAt(m_view->cursorPosition()); KTextEditor::Cursor cursorAfterPaste = pasteAt; QChar reg = getChosenRegister(UnnamedRegister); OperationMode m = getRegisterFlag(reg); QString textToInsert = getRegisterContent(reg); const bool isTextMultiLine = textToInsert.count(QLatin1Char('\n')) > 0; // In temporary normal mode, p/P act as gp/gP. isgPaste |= m_viInputModeManager->getTemporaryNormalMode(); if (textToInsert.isEmpty()) { error(i18n("Nothing in register %1", reg)); return false; } if (getCount() > 1) { textToInsert = textToInsert.repeated(getCount()); // FIXME: does this make sense for blocks? } if (m == LineWise) { pasteAt.setColumn(0); if (isIndentedPaste) { // Note that this does indeed work if there is no non-whitespace on the current line or if // the line is empty! const QString leadingWhiteSpaceOnCurrentLine = doc()->line(pasteAt.line()).mid(0, doc()->line(pasteAt.line()).indexOf(QRegExp(QLatin1String("[^\\s]")))); const QString leadingWhiteSpaceOnFirstPastedLine = textToInsert.mid(0, textToInsert.indexOf(QRegExp(QLatin1String("[^\\s]")))); // QString has no "left trim" method, bizarrely. while (textToInsert[0].isSpace()) { textToInsert = textToInsert.mid(1); } textToInsert.prepend(leadingWhiteSpaceOnCurrentLine); // Remove the last \n, temporarily: we're going to alter the indentation of each pasted line // by doing a search and replace on '\n's, but don't want to alter this one. textToInsert.chop(1); textToInsert.replace(QLatin1Char('\n') + leadingWhiteSpaceOnFirstPastedLine, QLatin1Char('\n') + leadingWhiteSpaceOnCurrentLine); textToInsert.append(QLatin1Char('\n')); // Re-add the temporarily removed last '\n'. } if (pasteLocation == AfterCurrentPosition) { textToInsert.chop(1); // remove the last \n pasteAt.setColumn(doc()->lineLength(pasteAt.line())); // paste after the current line and ... textToInsert.prepend(QLatin1Char('\n')); // ... prepend a \n, so the text starts on a new line cursorAfterPaste.setLine(cursorAfterPaste.line() + 1); } if (isgPaste) { cursorAfterPaste.setLine(cursorAfterPaste.line() + textToInsert.count(QLatin1Char('\n'))); } } else { if (pasteLocation == AfterCurrentPosition) { // Move cursor forward one before we paste. The position after the paste must also // be updated accordingly. if (getLine(pasteAt.line()).length() > 0) { pasteAt.setColumn(pasteAt.column() + 1); } cursorAfterPaste = pasteAt; } const bool leaveCursorAtStartOfPaste = isTextMultiLine && !isgPaste; if (!leaveCursorAtStartOfPaste) { cursorAfterPaste = cursorPosAtEndOfPaste(pasteAt, textToInsert); if (!isgPaste) { cursorAfterPaste.setColumn(cursorAfterPaste.column() - 1); } } } doc()->editBegin(); if (m_view->selection()) { pasteAt = m_view->selectionRange().start(); doc()->removeText(m_view->selectionRange()); } doc()->insertText(pasteAt, textToInsert, m == Block); doc()->editEnd(); if (cursorAfterPaste.line() >= doc()->lines()) { cursorAfterPaste.setLine(doc()->lines() - 1); } updateCursor(cursorAfterPaste); return true; } KTextEditor::Cursor NormalViMode::cursorPosAtEndOfPaste(const KTextEditor::Cursor &pasteLocation, const QString &pastedText) const { KTextEditor::Cursor cAfter = pasteLocation; const QStringList textLines = pastedText.split(QLatin1Char('\n')); if (textLines.length() == 1) { cAfter.setColumn(cAfter.column() + pastedText.length()); } else { cAfter.setColumn(textLines.last().length() - 0); cAfter.setLine(cAfter.line() + textLines.length() - 1); } return cAfter; } void NormalViMode::joinLines(unsigned int from, unsigned int to) const { // make sure we don't try to join lines past the document end if (to >= (unsigned int)(doc()->lines())) { to = doc()->lines() - 1; } // joining one line is a no-op if (from == to) { return; } doc()->joinLines(from, to); } void NormalViMode::reformatLines(unsigned int from, unsigned int to) const { joinLines(from, to); doc()->wrapText(from, to); } int NormalViMode::getFirstNonBlank(int line) const { if (line < 0) { line = m_view->cursorPosition().line(); } // doc()->plainKateTextLine returns NULL if the line is out of bounds. Kate::TextLine l = doc()->plainKateTextLine(line); Q_ASSERT(l); int c = l->firstChar(); return (c < 0) ? 0 : c; } // Tries to shrinks toShrink so that it fits tightly around rangeToShrinkTo. void NormalViMode::shrinkRangeAroundCursor(Range &toShrink, const Range &rangeToShrinkTo) const { if (!toShrink.valid || !rangeToShrinkTo.valid) { return; } KTextEditor::Cursor cursorPos = m_view->cursorPosition(); if (rangeToShrinkTo.startLine >= cursorPos.line()) { if (rangeToShrinkTo.startLine > cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.startLine == cursorPos.line()); if (rangeToShrinkTo.startColumn > cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (rangeToShrinkTo.endLine <= cursorPos.line()) { if (rangeToShrinkTo.endLine < cursorPos.line()) { // Does not surround cursor; aborting. return; } Q_ASSERT(rangeToShrinkTo.endLine == cursorPos.line()); if (rangeToShrinkTo.endColumn < cursorPos.column()) { // Does not surround cursor; aborting. return; } } if (toShrink.startLine <= rangeToShrinkTo.startLine) { if (toShrink.startLine < rangeToShrinkTo.startLine) { toShrink.startLine = rangeToShrinkTo.startLine; toShrink.startColumn = rangeToShrinkTo.startColumn; } Q_ASSERT(toShrink.startLine == rangeToShrinkTo.startLine); if (toShrink.startColumn < rangeToShrinkTo.startColumn) { toShrink.startColumn = rangeToShrinkTo.startColumn; } } if (toShrink.endLine >= rangeToShrinkTo.endLine) { if (toShrink.endLine > rangeToShrinkTo.endLine) { toShrink.endLine = rangeToShrinkTo.endLine; toShrink.endColumn = rangeToShrinkTo.endColumn; } Q_ASSERT(toShrink.endLine == rangeToShrinkTo.endLine); if (toShrink.endColumn > rangeToShrinkTo.endColumn) { toShrink.endColumn = rangeToShrinkTo.endColumn; } } } Range NormalViMode::textObjectComma(bool inner) const { // Basic algorithm: look left and right of the cursor for all combinations // of enclosing commas and the various types of brackets, and pick the pair // closest to the cursor that surrounds the cursor. Range r(0, 0, m_view->doc()->lines(), m_view->doc()->line(m_view->doc()->lastLine()).length(), InclusiveMotion); shrinkRangeAroundCursor(r, findSurroundingQuotes(QLatin1Char(','), inner)); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(')'), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char(']'), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char(','), QLatin1Char('}'), inner, QLatin1Char('{'), QLatin1Char('}'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('('), QLatin1Char(','), inner, QLatin1Char('('), QLatin1Char(')'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('['), QLatin1Char(','), inner, QLatin1Char('['), QLatin1Char(']'))); shrinkRangeAroundCursor(r, findSurroundingBrackets(QLatin1Char('{'), QLatin1Char(','), inner, QLatin1Char('{'), QLatin1Char('}'))); return r; } void NormalViMode::updateYankHighlightAttrib() { if (!m_highlightYankAttribute) { m_highlightYankAttribute = new KTextEditor::Attribute; } const QColor &yankedColor = m_view->renderer()->config()->savedLineColor(); m_highlightYankAttribute->setBackground(yankedColor); KTextEditor::Attribute::Ptr mouseInAttribute(new KTextEditor::Attribute()); mouseInAttribute->setFontBold(true); m_highlightYankAttribute->setDynamicAttribute(KTextEditor::Attribute::ActivateMouseIn, mouseInAttribute); m_highlightYankAttribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)->setBackground(yankedColor); } void NormalViMode::highlightYank(const Range &range, const OperationMode mode) { clearYankHighlight(); // current MovingRange doesn't support block mode selection so split the // block range into per-line ranges if (mode == Block) { for (int i = range.startLine; i <= range.endLine; i++) { addHighlightYank(KTextEditor::Range(i, range.startColumn, i, range.endColumn)); } } else { addHighlightYank(KTextEditor::Range(range.startLine, range.startColumn, range.endLine, range.endColumn)); } } void NormalViMode::addHighlightYank(const KTextEditor::Range &yankRange) { KTextEditor::MovingRange *highlightedYank = m_view->doc()->newMovingRange(yankRange, Kate::TextRange::DoNotExpand); highlightedYank->setView(m_view); // show only in this view highlightedYank->setAttributeOnlyForViews(true); // use z depth defined in moving ranges interface highlightedYank->setZDepth(-10000.0); highlightedYank->setAttribute(m_highlightYankAttribute); highlightedYankForDocument().insert(highlightedYank); } void NormalViMode::clearYankHighlight() { QSet &pHighlightedYanks = highlightedYankForDocument(); qDeleteAll(pHighlightedYanks); pHighlightedYanks.clear(); } void NormalViMode::aboutToDeleteMovingInterfaceContent() { QSet &pHighlightedYanks = highlightedYankForDocument(); // Prevent double-deletion in case this NormalMode is deleted. pHighlightedYanks.clear(); } QSet &NormalViMode::highlightedYankForDocument() { // Work around the fact that both Normal and Visual mode will have their own m_highlightedYank - // make Normal's the canonical one. return m_viInputModeManager->getViNormalMode()->m_highlightedYanks; } bool NormalViMode::waitingForRegisterOrCharToSearch() { // r, q, @ are never preceded by operators. There will always be a keys size of 1 for them. // f, t, F, T can be preceded by a delete/replace/yank/indent operator. size = 2 in that case. // f, t, F, T can be preceded by 'g' case/formatting operators. size = 3 in that case. const int keysSize = m_keys.size(); if (keysSize < 1) { // Just being defensive there. return false; } if (keysSize > 1) { // Multi-letter operation. QChar cPrefix = m_keys[0]; if (keysSize == 2) { // delete/replace/yank/indent operator? if (cPrefix != QLatin1Char('c') && cPrefix != QLatin1Char('d') && cPrefix != QLatin1Char('y') && cPrefix != QLatin1Char('=') && cPrefix != QLatin1Char('>') && cPrefix != QLatin1Char('<')) { return false; } } else if (keysSize == 3) { // We need to look deeper. Is it a g motion? QChar cNextfix = m_keys[1]; if (cPrefix != QLatin1Char('g') || ( cNextfix != QLatin1Char('U') && cNextfix != QLatin1Char('u') && cNextfix != QLatin1Char('~') && cNextfix != QLatin1Char('q') && cNextfix != QLatin1Char('w') && cNextfix != QLatin1Char('@'))) { return false; } } else { return false; } } QChar ch = m_keys[keysSize - 1]; return (ch == QLatin1Char('f') || ch == QLatin1Char('t') || ch == QLatin1Char('F') || ch == QLatin1Char('T') // c/d prefix unapplicable for the following cases. || (keysSize == 1 && (ch == QLatin1Char('r') || ch == QLatin1Char('q') || ch == QLatin1Char('@')))); } void NormalViMode::textInserted(KTextEditor::Document *document, KTextEditor::Range range) { Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); const bool continuesInsertion = range.start().line() == m_currentChangeEndMarker.line() && range.start().column() == m_currentChangeEndMarker.column(); const bool beginsWithNewline = doc()->text(range).at(0) == QLatin1Char('\n'); if (!continuesInsertion) { KTextEditor::Cursor newBeginMarkerPos = range.start(); if (beginsWithNewline && !isInsertReplaceMode) { // Presumably a linewise paste, in which case we ignore the leading '\n' newBeginMarkerPos = KTextEditor::Cursor(newBeginMarkerPos.line() + 1, 0); } m_viInputModeManager->marks()->setStartEditYanked(newBeginMarkerPos); } m_viInputModeManager->marks()->setLastChange(range.start()); KTextEditor::Cursor editEndMarker = range.end(); if (!isInsertReplaceMode) { editEndMarker.setColumn(editEndMarker.column() - 1); } m_viInputModeManager->marks()->setFinishEditYanked(editEndMarker); m_currentChangeEndMarker = range.end(); if (m_isUndo) { const bool addsMultipleLines = range.start().line() != range.end().line(); m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line(), 0)); if (addsMultipleLines) { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + 1, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + 1, 0)); } else { m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line(), 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line(), 0)); } } } void NormalViMode::textRemoved(KTextEditor::Document *document, KTextEditor::Range range) { Q_UNUSED(document); const bool isInsertReplaceMode = (m_viInputModeManager->getCurrentViMode() == ViMode::InsertMode || m_viInputModeManager->getCurrentViMode() == ViMode::ReplaceMode); m_viInputModeManager->marks()->setLastChange(range.start()); if (!isInsertReplaceMode) { // Don't go resetting [ just because we did a Ctrl-h! m_viInputModeManager->marks()->setStartEditYanked(range.start()); } else { // Don't go disrupting our continued insertion just because we did a Ctrl-h! m_currentChangeEndMarker = range.start(); } m_viInputModeManager->marks()->setFinishEditYanked(range.start()); if (m_isUndo) { // Slavishly follow Vim's weird rules: if an undo removes several lines, then all markers should // be at the beginning of the line after the last line removed, else they should at the beginning // of the line above that. const int markerLineAdjustment = (range.start().line() != range.end().line()) ? 1 : 0; m_viInputModeManager->marks()->setStartEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getStartEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setFinishEditYanked(KTextEditor::Cursor(m_viInputModeManager->marks()->getFinishEditYanked().line() + markerLineAdjustment, 0)); m_viInputModeManager->marks()->setLastChange(KTextEditor::Cursor(m_viInputModeManager->marks()->getLastChange().line() + markerLineAdjustment, 0)); } } void NormalViMode::undoBeginning() { m_isUndo = true; } void NormalViMode::undoEnded() { m_isUndo = false; } bool NormalViMode::executeKateCommand(const QString &command) { KTextEditor::Command *p = KateCmd::self()->queryCommand(command); if (!p) { return false; } QString msg; return p->exec(m_view, command, msg); }