diff --git a/autotests/syntaxrepository_test.cpp b/autotests/syntaxrepository_test.cpp index 2d0f42a..4a3902a 100644 --- a/autotests/syntaxrepository_test.cpp +++ b/autotests/syntaxrepository_test.cpp @@ -1,316 +1,315 @@ /* Copyright (C) 2016 Volker Krause This program 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "test-config.h" #include #include #include #include #include #include #include #include #include #include namespace KSyntaxHighlighting { class NullHighlighter : public AbstractHighlighter { public: using AbstractHighlighter::highlightLine; void applyFormat(int offset, int length, const Format &format) Q_DECL_OVERRIDE { Q_UNUSED(offset); Q_UNUSED(length); // only here to ensure we don't crash format.isDefaultTextStyle(theme()); format.textColor(theme()); } }; class RepositoryTest : public QObject { Q_OBJECT private: Repository m_repo; private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); } void testDefinitionByExtension_data() { QTest::addColumn("fileName"); QTest::addColumn("defName"); QTest::newRow("empty") << QString() << QString(); QTest::newRow("qml") << QStringLiteral("/bla/foo.qml") << QStringLiteral("QML"); QTest::newRow("glsl") << QStringLiteral("flat.frag") << QStringLiteral("GLSL"); // the following ones are defined in multiple syntax definitions QTest::newRow("c") << QStringLiteral("test.c") << QStringLiteral("C"); QTest::newRow("c++") << QStringLiteral("test.cpp") << QStringLiteral("C++"); QTest::newRow("markdown") << QStringLiteral("test.md") << QStringLiteral("Markdown"); QTest::newRow("Makefile 1") << QStringLiteral("Makefile") << QStringLiteral("Makefile"); QTest::newRow("Makefile 2") << QStringLiteral("/some/path/to/Makefile") << QStringLiteral("Makefile"); QTest::newRow("Makefile 3") << QStringLiteral("Makefile.am") << QStringLiteral("Makefile"); } void testDefinitionByExtension() { QFETCH(QString, fileName); QFETCH(QString, defName); auto def = m_repo.definitionForFileName(fileName); if (defName.isEmpty()) { QVERIFY(!def.isValid()); } else { QVERIFY(def.isValid()); QCOMPARE(def.name(), defName); } } void testLoadAll() { foreach (const auto &def, m_repo.definitions()) { QVERIFY(!def.name().isEmpty()); QVERIFY(!def.translatedName().isEmpty()); - QVERIFY(!def.section().isEmpty()); - QVERIFY(!def.translatedSection().isEmpty()); + QVERIFY(!def.isValid() || !def.section().isEmpty()); + QVERIFY(!def.isValid() || !def.translatedSection().isEmpty()); // indirectly trigger loading, as we can't reach that from public API // if the loading fails the highlighter will produce empty states NullHighlighter hl; State initialState; hl.setDefinition(def); const auto state = hl.highlightLine(QLatin1String("This should not crash } ] ) !"), initialState); - QVERIFY(state != initialState); + QVERIFY(!def.isValid() || state != initialState); } } void testMetaData() { auto def = m_repo.definitionForName(QLatin1String("Alerts")); QVERIFY(def.isValid()); QVERIFY(def.extensions().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.version() >= 1.11f); QVERIFY(def.isHidden()); QCOMPARE(def.section(), QLatin1String("Other")); QCOMPARE(def.license(), QLatin1String("MIT")); QVERIFY(def.author().contains(QLatin1String("Dominik"))); QFileInfo fi(def.filePath()); QVERIFY(fi.isAbsolute()); QVERIFY(def.filePath().endsWith(QLatin1String("alert.xml"))); def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QCOMPARE(def.section(), QLatin1String("Sources")); QCOMPARE(def.indenter(), QLatin1String("cstyle")); QCOMPARE(def.style(), QLatin1String("C++")); QVERIFY(def.mimeTypes().contains(QLatin1String("text/x-c++hdr"))); QVERIFY(def.extensions().contains(QLatin1String("*.h"))); QCOMPARE(def.priority(), 9); def = m_repo.definitionForName(QLatin1String("Apache Configuration")); QVERIFY(def.isValid()); QVERIFY(def.extensions().contains(QLatin1String("httpd.conf"))); QVERIFY(def.extensions().contains(QLatin1String(".htaccess*"))); } void testGeneralMetaData() { auto def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QVERIFY(!def.indentationBasedFoldingEnabled()); // comment markers QCOMPARE(def.singleLineCommentMarker(), QLatin1String("//")); QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); const auto cppMultiLineCommentMarker = QPair(QLatin1String("/*"), QLatin1String("*/")); QCOMPARE(def.multiLineCommentMarker(), cppMultiLineCommentMarker); def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); // indentation QVERIFY(def.indentationBasedFoldingEnabled()); QCOMPARE(def.foldingIgnoreList(), QStringList() << QLatin1String("(?:\\s+|\\s*#.*)")); // keyword lists QVERIFY(!def.keywordLists().isEmpty()); QVERIFY(def.keywordList(QLatin1String("operators")).contains(QLatin1String("and"))); QVERIFY(!def.keywordList(QLatin1String("does not exits")).contains(QLatin1String("and"))); } void testFormatData() { auto def = m_repo.definitionForName(QLatin1String("ChangeLog")); QVERIFY(def.isValid()); auto formats = def.formats(); QVERIFY(!formats.isEmpty()); // verify that the formats are sorted, such that the order matches the order of the itemDatas in the xml files. auto sortComparator = [](const KSyntaxHighlighting::Format & lhs, const KSyntaxHighlighting::Format & rhs) { return lhs.id() < rhs.id(); }; QVERIFY(std::is_sorted(formats.begin(), formats.end(), sortComparator)); // check all names are listed QStringList formatNames; foreach (const auto & format, formats) { formatNames.append(format.name()); } const QStringList expectedItemDatas = { QStringLiteral("Normal Text"), QStringLiteral("Name"), QStringLiteral("E-Mail"), QStringLiteral("Date"), QStringLiteral("Entry") }; QCOMPARE(formatNames, expectedItemDatas); } void testIncludedDefinitions() { auto def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); auto defs = def.includedDefinitions(); const QStringList expectedDefinitionNames = { QStringLiteral("ISO C++"), QStringLiteral("GCCExtensions"), QStringLiteral("Doxygen"), QStringLiteral("Alerts"), QStringLiteral("Modelines") }; QStringList definitionNames; for (auto d : defs) { QVERIFY(d.isValid()); definitionNames.push_back(d.name()); } QCOMPARE(definitionNames, expectedDefinitionNames); } void testReload() { auto def = m_repo.definitionForName(QLatin1String("QML")); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(def.isValid()); NullHighlighter hl; hl.setDefinition(def); auto oldState = hl.highlightLine(QLatin1String("/* TODO this should not crash */"), State()); m_repo.reload(); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(!def.isValid()); hl.highlightLine(QLatin1String("/* TODO this should not crash */"), State()); hl.highlightLine(QLatin1String("/* FIXME neither should this crash */"), oldState); QVERIFY(hl.definition().isValid()); QCOMPARE(hl.definition().name(), QLatin1String("QML")); } void testLifetime() { // common mistake with value-type like Repo API, make sure this doesn'T crash NullHighlighter hl; { Repository repo; hl.setDefinition(repo.definitionForName(QLatin1String("C++"))); hl.setTheme(repo.defaultTheme()); } hl.highlightLine(QLatin1String("/**! @todo this should not crash .*/"), State()); } void testCustomPath() { QString testInputPath = QStringLiteral(TESTSRCDIR "/input"); Repository repo; QVERIFY(repo.customSearchPaths().empty()); repo.addCustomSearchPath(testInputPath); QCOMPARE(repo.customSearchPaths().size(), 1); QCOMPARE(repo.customSearchPaths()[0], testInputPath); auto customDefinition = repo.definitionForName(QLatin1String("Test Syntax")); QVERIFY(customDefinition.isValid()); auto customTheme = repo.theme(QLatin1String("Test Theme")); QVERIFY(customTheme.isValid()); } void testInvalidDefinition() { Definition def; QVERIFY(!def.isValid()); QVERIFY(def.filePath().isEmpty()); - QVERIFY(def.name().isEmpty()); - QVERIFY(def.translatedName().isEmpty()); + QCOMPARE(def.name(), QLatin1String("None")); QVERIFY(def.section().isEmpty()); QVERIFY(def.translatedSection().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.extensions().isEmpty()); QCOMPARE(def.version(), 0); QCOMPARE(def.priority(), 0); QVERIFY(!def.isHidden()); QVERIFY(def.style().isEmpty()); QVERIFY(def.indenter().isEmpty()); QVERIFY(def.author().isEmpty()); QVERIFY(def.license().isEmpty()); QVERIFY(!def.indentationBasedFoldingEnabled()); QVERIFY(def.foldingIgnoreList().isEmpty()); QVERIFY(def.keywordLists().isEmpty()); QVERIFY(def.formats().isEmpty()); QVERIFY(def.includedDefinitions().isEmpty()); QVERIFY(def.singleLineCommentMarker().isEmpty()); QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); const auto emptyPair = QPair(); QCOMPARE(def.multiLineCommentMarker(), emptyPair); for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) { QVERIFY(def.isWordDelimiter(c)); QVERIFY(def.isWordWrapDelimiter(c)); } } void testDelimiters() { auto def = m_repo.definitionForName(QLatin1String("LaTeX")); QVERIFY(def.isValid()); // check that backslash '\' is removed for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[]^{|}~")) QVERIFY(def.isWordDelimiter(c)); QVERIFY(!def.isWordDelimiter(QLatin1Char('\\'))); // check where breaking a line is valid for (QChar c : QStringLiteral(",{}[]")) QVERIFY(def.isWordWrapDelimiter(c)); } }; } QTEST_GUILESS_MAIN(KSyntaxHighlighting::RepositoryTest) #include "syntaxrepository_test.moc" diff --git a/autotests/testhighlighter.cpp b/autotests/testhighlighter.cpp index ed030e4..04a50b4 100644 --- a/autotests/testhighlighter.cpp +++ b/autotests/testhighlighter.cpp @@ -1,190 +1,190 @@ /* Copyright (C) 2016 Volker Krause This program 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "test-config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; class TestHighlighter : public AbstractHighlighter { public: void highlightFile(const QString &inFileName, const QString &outFileName) { QFile outFile(outFileName); if (!outFile.open(QFile::WriteOnly | QFile::Truncate)) { qWarning() << "Failed to open output file" << outFileName << ":" << outFile.errorString(); return; } m_out.setDevice(&outFile); m_out.setCodec("UTF-8"); QFile f(inFileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open input file" << inFileName << ":" << f.errorString(); return; } QTextStream in(&f); in.setCodec("UTF-8"); State state; while (!in.atEnd()) { m_currentLine = in.readLine(); state = highlightLine(m_currentLine, state); m_out << "
\n"; } m_out.flush(); } protected: void applyFormat(int offset, int length, const Format &format) Q_DECL_OVERRIDE { if (format.name().isEmpty()) m_out << "" << m_currentLine.midRef(offset, length) << ""; else m_out << "<" << format.name() << ">" << m_currentLine.midRef(offset, length) << ""; } private: QTextStream m_out; QString m_currentLine; }; class TestHighlighterTest : public QObject { Q_OBJECT public: explicit TestHighlighterTest(QObject *parent = nullptr) : QObject(parent), m_repo(nullptr) {} private: Repository *m_repo; QSet m_coveredDefinitions; private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); m_repo = new Repository; } void cleanupTestCase() { QFile coveredList(QLatin1String(TESTBUILDDIR "/covered-definitions.txt")); QFile uncoveredList(QLatin1String(TESTBUILDDIR "/uncovered-definition.txt")); QVERIFY(coveredList.open(QFile::WriteOnly)); QVERIFY(uncoveredList.open(QFile::WriteOnly)); int count = 0; foreach (const auto &def, m_repo->definitions()) { - if (def.isHidden()) + if (def.isHidden() || !def.isValid()) continue; ++count; if (m_coveredDefinitions.contains(def.name())) coveredList.write(def.name().toUtf8() + '\n'); else uncoveredList.write(def.name().toUtf8() + '\n'); } qDebug() << "Syntax definitions with test coverage:" << ((float)m_coveredDefinitions.size() * 100.0f / (float)count) << "%"; delete m_repo; m_repo = nullptr; } void testHighlight_data() { QTest::addColumn("inFile"); QTest::addColumn("outFile"); QTest::addColumn("refFile"); QTest::addColumn("syntax"); QDirIterator it(QStringLiteral(TESTSRCDIR "/input"), QDir::Files | QDir::NoSymLinks | QDir::Readable); while (it.hasNext()) { const auto inFile = it.next(); if (inFile.endsWith(QLatin1String(".syntax"))) continue; QString syntax; QFile syntaxOverride(inFile + QStringLiteral(".syntax")); if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); QTest::newRow(it.fileName().toUtf8().constData()) << inFile << (QStringLiteral(TESTBUILDDIR "/output/") + it.fileName() + QStringLiteral(".ref")) << (QStringLiteral(TESTSRCDIR "/reference/") + it.fileName() + QStringLiteral(".ref")) << syntax; } QVERIFY(QDir().mkpath(QStringLiteral(TESTBUILDDIR "/output/"))); } void testHighlight() { QFETCH(QString, inFile); QFETCH(QString, outFile); QFETCH(QString, refFile); QFETCH(QString, syntax); QVERIFY(m_repo); auto def = m_repo->definitionForFileName(inFile); if (!syntax.isEmpty()) def = m_repo->definitionForName(syntax); TestHighlighter highlighter; highlighter.setTheme(m_repo->defaultTheme()); QVERIFY(highlighter.theme().isValid()); QVERIFY(def.isValid()); qDebug() << "Using syntax" << def.name(); m_coveredDefinitions.insert(def.name()); highlighter.setDefinition(def); highlighter.highlightFile(inFile, outFile); const auto diffExecutable = QStandardPaths::findExecutable(QStringLiteral("diff")); if (!diffExecutable.isEmpty()) { QProcess proc; proc.setProcessChannelMode(QProcess::ForwardedChannels); proc.start(diffExecutable, {QStringLiteral("-u"), refFile, outFile}); QVERIFY(proc.waitForFinished()); QCOMPARE(proc.exitCode(), 0); } else { qDebug() << "Skipping part of the test since the 'diff' executable is not in PATH"; } } }; QTEST_GUILESS_MAIN(TestHighlighterTest) #include "testhighlighter.moc" diff --git a/examples/codeeditor.cpp b/examples/codeeditor.cpp index 950dd90..8781583 100644 --- a/examples/codeeditor.cpp +++ b/examples/codeeditor.cpp @@ -1,356 +1,352 @@ /* Copyright (C) 2016 Volker Krause This program 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "codeeditor.h" #include #include #include #include #include #include #include #include #include #include #include #include class CodeEditorSidebar : public QWidget { Q_OBJECT public: explicit CodeEditorSidebar(CodeEditor *editor); QSize sizeHint() const Q_DECL_OVERRIDE; protected: void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE; private: CodeEditor *m_codeEditor; }; CodeEditorSidebar::CodeEditorSidebar(CodeEditor *editor) : QWidget(editor), m_codeEditor(editor) { } QSize CodeEditorSidebar::sizeHint() const { return QSize(m_codeEditor->sidebarWidth(), 0); } void CodeEditorSidebar::paintEvent(QPaintEvent *event) { m_codeEditor->sidebarPaintEvent(event); } void CodeEditorSidebar::mouseReleaseEvent(QMouseEvent *event) { if (event->x() >= width() - m_codeEditor->fontMetrics().lineSpacing()) { auto block = m_codeEditor->blockAtPosition(event->y()); if (!block.isValid() || !m_codeEditor->isFoldable(block)) return; m_codeEditor->toggleFold(block); } QWidget::mouseReleaseEvent(event); } CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent), m_highlighter(new KSyntaxHighlighting::SyntaxHighlighter(document())), m_sideBar(new CodeEditorSidebar(this)) { setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); setTheme((palette().color(QPalette::Base).lightness() < 128) ? m_repository.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme) : m_repository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme)); connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::updateSidebarGeometry); connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::updateSidebarArea); connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine); updateSidebarGeometry(); highlightCurrentLine(); } CodeEditor::~CodeEditor() { } void CodeEditor::openFile(const QString& fileName) { QFile f(fileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open" << fileName << ":" << f.errorString(); return; } clear(); const auto def = m_repository.definitionForFileName(fileName); m_highlighter->setDefinition(def); setWindowTitle(fileName); setPlainText(QString::fromUtf8(f.readAll())); } void CodeEditor::contextMenuEvent(QContextMenuEvent *event) { auto menu = createStandardContextMenu(event->pos()); menu->addSeparator(); auto openAction = menu->addAction(QStringLiteral("Open File...")); connect(openAction, &QAction::triggered, this, [this]() { const auto fileName = QFileDialog::getOpenFileName(this, QStringLiteral("Open File")); if (!fileName.isEmpty()) openFile(fileName); }); // syntax selection auto hlActionGroup = new QActionGroup(menu); hlActionGroup->setExclusive(true); auto hlGroupMenu = menu->addMenu(QStringLiteral("Syntax")); - auto noHlAction = hlGroupMenu->addAction(QStringLiteral("None")); - noHlAction->setCheckable(true); - hlActionGroup->addAction(noHlAction); - noHlAction->setChecked(!m_highlighter->definition().isValid()); - QMenu *hlSubMenu = nullptr; + QMenu *hlSubMenu = hlGroupMenu; QString currentGroup; foreach (const auto &def, m_repository.definitions()) { if (def.isHidden()) continue; if (currentGroup != def.section()) { currentGroup = def.section(); hlSubMenu = hlGroupMenu->addMenu(def.translatedSection()); } Q_ASSERT(hlSubMenu); auto action = hlSubMenu->addAction(def.translatedName()); action->setCheckable(true); action->setData(def.name()); hlActionGroup->addAction(action); if (def.name() == m_highlighter->definition().name()) action->setChecked(true); } connect(hlActionGroup, &QActionGroup::triggered, this, [this](QAction *action) { const auto defName = action->data().toString(); const auto def = m_repository.definitionForName(defName); m_highlighter->setDefinition(def); }); // theme selection auto themeGroup = new QActionGroup(menu); themeGroup->setExclusive(true); auto themeMenu = menu->addMenu(QStringLiteral("Theme")); foreach (const auto &theme, m_repository.themes()) { auto action = themeMenu->addAction(theme.translatedName()); action->setCheckable(true); action->setData(theme.name()); themeGroup->addAction(action); if (theme.name() == m_highlighter->theme().name()) action->setChecked(true); } connect(themeGroup, &QActionGroup::triggered, this, [this](QAction *action) { const auto themeName = action->data().toString(); const auto theme = m_repository.theme(themeName); setTheme(theme); }); menu->exec(event->globalPos()); delete menu; } void CodeEditor::resizeEvent(QResizeEvent *event) { QPlainTextEdit::resizeEvent(event); updateSidebarGeometry(); } void CodeEditor::setTheme(const KSyntaxHighlighting::Theme &theme) { auto pal = qApp->palette(); if (theme.isValid()) { pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor)); pal.setColor(QPalette::Text, theme.textColor(KSyntaxHighlighting::Theme::Normal)); pal.setColor(QPalette::Highlight, theme.editorColor(KSyntaxHighlighting::Theme::TextSelection)); } setPalette(pal); m_highlighter->setTheme(theme); m_highlighter->rehighlight(); highlightCurrentLine(); } int CodeEditor::sidebarWidth() const { int digits = 1; auto count = blockCount(); while (count >= 10) { ++digits; count /= 10; } return 4 + fontMetrics().width(QLatin1Char('9')) * digits + fontMetrics().lineSpacing(); } void CodeEditor::sidebarPaintEvent(QPaintEvent *event) { QPainter painter(m_sideBar); painter.fillRect(event->rect(), m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::IconBorder)); auto block = firstVisibleBlock(); auto blockNumber = block.blockNumber(); int top = blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + blockBoundingRect(block).height(); const int currentBlockNumber = textCursor().blockNumber(); const auto foldingMarkerSize = fontMetrics().lineSpacing(); while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { const auto number = QString::number(blockNumber + 1); painter.setPen(m_highlighter->theme().editorColor( (blockNumber == currentBlockNumber) ? KSyntaxHighlighting::Theme::CurrentLineNumber : KSyntaxHighlighting::Theme::LineNumbers)); painter.drawText(0, top, m_sideBar->width() - 2 - foldingMarkerSize, fontMetrics().height(), Qt::AlignRight, number); } // folding marker if (block.isVisible() && isFoldable(block)) { QPolygonF polygon; if (isFolded(block)) { polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.25); polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.75); polygon << QPointF(foldingMarkerSize * 0.8, foldingMarkerSize * 0.5); } else { polygon << QPointF(foldingMarkerSize * 0.25, foldingMarkerSize * 0.4); polygon << QPointF(foldingMarkerSize * 0.75, foldingMarkerSize * 0.4); polygon << QPointF(foldingMarkerSize * 0.5, foldingMarkerSize * 0.8); } painter.save(); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(Qt::NoPen); painter.setBrush(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CodeFolding))); painter.translate(m_sideBar->width() - foldingMarkerSize, top); painter.drawPolygon(polygon); painter.restore(); } block = block.next(); top = bottom; bottom = top + blockBoundingRect(block).height(); ++blockNumber; } } void CodeEditor::updateSidebarGeometry() { setViewportMargins(sidebarWidth(), 0, 0, 0); const auto r = contentsRect(); m_sideBar->setGeometry(QRect(r.left(), r.top(), sidebarWidth(), r.height())); } void CodeEditor::updateSidebarArea(const QRect& rect, int dy) { if (dy) m_sideBar->scroll(0, dy); else m_sideBar->update(0, rect.y(), m_sideBar->width(), rect.height()); } void CodeEditor::highlightCurrentLine() { QTextEdit::ExtraSelection selection; selection.format.setBackground(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CurrentLine))); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); QList extraSelections; extraSelections.append(selection); setExtraSelections(extraSelections); } QTextBlock CodeEditor::blockAtPosition(int y) const { auto block = firstVisibleBlock(); if (!block.isValid()) return QTextBlock(); int top = blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + blockBoundingRect(block).height(); do { if (top <= y && y <= bottom) return block; block = block.next(); top = bottom; bottom = top + blockBoundingRect(block).height(); } while (block.isValid()); return QTextBlock(); } bool CodeEditor::isFoldable(const QTextBlock &block) const { return m_highlighter->startsFoldingRegion(block); } bool CodeEditor::isFolded(const QTextBlock &block) const { if (!block.isValid()) return false; const auto nextBlock = block.next(); if (!nextBlock.isValid()) return false; return !nextBlock.isVisible(); } void CodeEditor::toggleFold(const QTextBlock &startBlock) { // we also want to fold the last line of the region, therefore the ".next()" const auto endBlock = m_highlighter->findFoldingRegionEnd(startBlock).next(); if (isFolded(startBlock)) { // unfold auto block = startBlock.next(); while (block.isValid() && !block.isVisible()) { block.setVisible(true); block.setLineCount(block.layout()->lineCount()); block = block.next(); } } else { // fold auto block = startBlock.next(); while (block.isValid() && block != endBlock) { block.setVisible(false); block.setLineCount(0); block = block.next(); } } // redraw document document()->markContentsDirty(startBlock.position(), endBlock.position() - startBlock.position() + 1); // update scrollbars emit document()->documentLayout()->documentSizeChanged(document()->documentLayout()->documentSize()); } #include "codeeditor.moc" diff --git a/src/lib/definition_p.h b/src/lib/definition_p.h index 98bcfde..9ad7705 100644 --- a/src/lib/definition_p.h +++ b/src/lib/definition_p.h @@ -1,102 +1,102 @@ /* Copyright (C) 2016 Volker Krause This program 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KSYNTAXHIGHLIGHTING_DEFINITION_P_H #define KSYNTAXHIGHLIGHTING_DEFINITION_P_H #include "definitionref_p.h" #include "definition.h" #include #include #include QT_BEGIN_NAMESPACE class QXmlStreamReader; class QJsonObject; QT_END_NAMESPACE namespace KSyntaxHighlighting { class Repository; class DefinitionData { public: DefinitionData(); ~DefinitionData(); static DefinitionData* get(const Definition &def); bool isLoaded() const; bool loadMetaData(const QString &definitionFileName); bool loadMetaData(const QString &fileName, const QJsonObject &obj); void clear(); bool load(); bool loadLanguage(QXmlStreamReader &reader); void loadHighlighting(QXmlStreamReader &reader); void loadContexts(QXmlStreamReader &reader); void loadItemData(QXmlStreamReader &reader); void loadGeneral(QXmlStreamReader &reader); void loadComments(QXmlStreamReader &reader); void loadFoldingIgnoreList(QXmlStreamReader &reader); bool checkKateVersion(const QStringRef &verStr); KeywordList keywordList(const QString &name) const; bool isWordDelimiter(QChar c) const; Context* initialContext() const; Context* contextByName(const QString &name) const; Format formatByName(const QString &name) const; quint16 foldingRegionId(const QString &foldName); DefinitionRef q; Repository *repo = nullptr; QHash keywordLists; QVector contexts; QHash formats; QString wordDelimiters; QString wordWrapDelimiters; bool indentationBasedFolding = false; QStringList foldingIgnoreList; QString singleLineCommentMarker; CommentPosition singleLineCommentPosition = CommentPosition::StartOfLine; QString multiLineCommentStartMarker; QString multiLineCommentEndMarker; QString fileName; - QString name; + QString name = QStringLiteral(QT_TRANSLATE_NOOP("Syntax highlighting", "None")); QString section; QString style; QString indenter; QString author; QString license; QVector mimetypes; QVector extensions; Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive; int version = 0; int priority = 0; bool hidden = false; }; } #endif diff --git a/src/lib/repository.cpp b/src/lib/repository.cpp index 4ab6dd9..012a63f 100644 --- a/src/lib/repository.cpp +++ b/src/lib/repository.cpp @@ -1,279 +1,282 @@ /* Copyright (C) 2016 Volker Krause This program 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 program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "repository.h" #include "repository_p.h" #include "definition.h" #include "definition_p.h" #include "theme.h" #include "themedata_p.h" #include "ksyntaxhighlighting_logging.h" #include "wildcardmatcher_p.h" #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; static void initResource() { Q_INIT_RESOURCE(syntax_data); } RepositoryPrivate* RepositoryPrivate::get(Repository *repo) { return repo->d.get(); } Repository::Repository() : d(new RepositoryPrivate) { initResource(); d->load(this); } Repository::~Repository() { // reset repo so we can detect in still alive definition instances // that the repo was deleted foreach (const auto &def, d->m_sortedDefs) DefinitionData::get(def)->repo = nullptr; } Definition Repository::definitionForName(const QString& defName) const { return d->m_defs.value(defName); } Definition Repository::definitionForFileName(const QString& fileName) const { QFileInfo fi(fileName); const auto name = fi.fileName(); QVector candidates; for (auto it = d->m_defs.constBegin(); it != d->m_defs.constEnd(); ++it) { auto def = it.value(); foreach (const auto &pattern, def.extensions()) { if (WildcardMatcher::exactMatch(name, pattern)) { candidates.push_back(def); break; } } } if (candidates.isEmpty()) return Definition(); std::partial_sort(candidates.begin(), candidates.begin() + 1, candidates.end(), [](const Definition &lhs, const Definition &rhs) { return lhs.priority() > rhs.priority(); }); return candidates.at(0); } QVector Repository::definitions() const { return d->m_sortedDefs; } QVector Repository::themes() const { return d->m_themes; } Theme Repository::theme(const QString &themeName) const { for (const auto &theme : d->m_themes) { if (theme.name() == themeName) { return theme; } } return Theme(); } Theme Repository::defaultTheme(Repository::DefaultTheme t) { if (t == DarkTheme) return theme(QLatin1String("Breeze Dark")); return theme(QLatin1String("Default")); } void RepositoryPrivate::load(Repository *repo) { + // always add invalid default "None" highlighting + addDefinition(Definition()); + auto dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/syntax"), QStandardPaths::LocateDirectory); foreach (const auto &dir, dirs) loadSyntaxFolder(repo, dir); // backward compatibility with Kate dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("katepart5/syntax"), QStandardPaths::LocateDirectory); foreach (const auto &dir, dirs) loadSyntaxFolder(repo, dir); loadSyntaxFolder(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax")); foreach (const auto &path, m_customSearchPaths) loadSyntaxFolder(repo, path + QStringLiteral("/syntax")); m_sortedDefs.reserve(m_defs.size()); for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) m_sortedDefs.push_back(it.value()); std::sort(m_sortedDefs.begin(), m_sortedDefs.end(), [](const Definition &left, const Definition &right) { auto comparison = left.translatedSection().compare(right.translatedSection(), Qt::CaseInsensitive); if (comparison == 0) comparison = left.translatedName().compare(right.translatedName(), Qt::CaseInsensitive); return comparison < 0; }); // load themes dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("org.kde.syntax-highlighting/themes"), QStandardPaths::LocateDirectory); foreach (const auto &dir, dirs) loadThemeFolder(dir); loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes")); foreach (const auto &path, m_customSearchPaths) loadThemeFolder(path + QStringLiteral("/themes")); } void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path) { if (loadSyntaxFolderFromIndex(repo, path)) return; QDirIterator it(path, QStringList() << QLatin1String("*.xml"), QDir::Files); while (it.hasNext()) { Definition def; auto defData = DefinitionData::get(def); defData->repo = repo; if (defData->loadMetaData(it.next())) addDefinition(def); } } bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path) { QFile indexFile(path + QLatin1String("/index.katesyntax")); if (!indexFile.open(QFile::ReadOnly)) return false; const auto indexDoc(QJsonDocument::fromBinaryData(indexFile.readAll())); const auto index = indexDoc.object(); for (auto it = index.begin(); it != index.end(); ++it) { if (!it.value().isObject()) continue; const auto fileName = QString(path + QLatin1Char('/') + it.key()); const auto defMap = it.value().toObject(); Definition def; auto defData = DefinitionData::get(def); defData->repo = repo; if (defData->loadMetaData(fileName, defMap)) addDefinition(def); } return true; } void RepositoryPrivate::addDefinition(const Definition &def) { const auto it = m_defs.constFind(def.name()); if (it == m_defs.constEnd()) { m_defs.insert(def.name(), def); return; } if (it.value().version() >= def.version()) return; m_defs.insert(def.name(), def); } void RepositoryPrivate::loadThemeFolder(const QString &path) { QDirIterator it(path, QStringList() << QLatin1String("*.theme"), QDir::Files); while (it.hasNext()) { auto themeData = std::unique_ptr(new ThemeData); if (themeData->load(it.next())) addTheme(Theme(themeData.release())); } } static int themeRevision(const Theme &theme) { auto data = ThemeData::get(theme); return data->revision(); } void RepositoryPrivate::addTheme(const Theme &theme) { const auto it = std::lower_bound(m_themes.begin(), m_themes.end(), theme, [](const Theme &lhs, const Theme &rhs) { return lhs.name() < rhs.name(); }); if (it == m_themes.end() || (*it).name() != theme.name()) { m_themes.insert(it, theme); return; } if (themeRevision(*it) < themeRevision(theme)) *it = theme; } quint16 RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName) { const auto it = m_foldingRegionIds.constFind(qMakePair(defName, foldName)); if (it != m_foldingRegionIds.constEnd()) return it.value(); m_foldingRegionIds.insert(qMakePair(defName, foldName), ++m_foldingRegionId); return m_foldingRegionId; } quint16 RepositoryPrivate::nextFormatId() { Q_ASSERT(m_formatId < std::numeric_limits::max()); return ++m_formatId; } void Repository::reload() { qCDebug(Log) << "Reloading syntax definitions!"; foreach (const auto &def, d->m_sortedDefs) DefinitionData::get(def)->clear(); d->m_defs.clear(); d->m_sortedDefs.clear(); d->m_themes.clear(); d->m_foldingRegionId = 0; d->m_foldingRegionIds.clear(); d->m_formatId = 0; d->load(this); } void Repository::addCustomSearchPath(const QString &path) { d->m_customSearchPaths.append(path); reload(); } QVector Repository::customSearchPaths() const { return d->m_customSearchPaths; }