File indexing completed on 2024-05-05 05:52:19

0001 /*  This file is part of the Kate project.
0002  *  Based on the snippet plugin from KDevelop 4.
0003  *
0004  *  SPDX-FileCopyrightText: 2007 Robert Gruber <rgruber@users.sourceforge.net>
0005  *  SPDX-FileCopyrightText: 2010 Milian Wolff <mail@milianw.de>
0006  *  SPDX-FileCopyrightText: 2012 Christoph Cullmann <cullmann@kde.org>
0007  *
0008  *  SPDX-License-Identifier: LGPL-2.0-or-later
0009  */
0010 
0011 #include "snippetrepository.h"
0012 
0013 #include "snippet.h"
0014 
0015 #include <QAction>
0016 #include <QFile>
0017 #include <QFileInfo>
0018 #include <QTimer>
0019 
0020 #include <QDomDocument>
0021 #include <QDomElement>
0022 
0023 #include <KLocalizedString>
0024 #include <KMessageBox>
0025 #include <QApplication>
0026 #include <QDebug>
0027 
0028 #include <KColorScheme>
0029 
0030 #include <KUser>
0031 
0032 #include "snippetstore.h"
0033 
0034 static const QString defaultScript = QStringLiteral(
0035     "\
0036 function fileName() { return document.fileName(); }\n\
0037 function fileUrl() { return document.url(); }\n\
0038 function encoding() { return document.encoding(); }\n\
0039 function selection() { return view.selectedText(); }\n\
0040 function year() { return new Date().getFullYear(); }\n\
0041 function upper(x) { return x.toUpperCase(); }\n\
0042 function lower(x) { return x.toLowerCase(); }\n");
0043 
0044 SnippetRepository::SnippetRepository(const QString &file)
0045     : QStandardItem(i18n("<empty repository>"))
0046     , m_file(file)
0047     , m_script(defaultScript)
0048 {
0049     setIcon(QIcon::fromTheme(QStringLiteral("folder")));
0050     const auto &config = SnippetStore::self()->getConfig();
0051     bool activated = config.readEntry<QStringList>("enabledRepositories", QStringList()).contains(file);
0052     setCheckState(activated ? Qt::Checked : Qt::Unchecked);
0053 
0054     if (QFile::exists(file)) {
0055         // Tell the new repository to load it's snippets
0056         QTimer::singleShot(0, model(), [this] {
0057             parseFile();
0058         });
0059     }
0060 
0061     // qDebug() << "created new snippet repo" << file << this;
0062 }
0063 
0064 SnippetRepository::~SnippetRepository()
0065 {
0066     // remove all our children from both the model and our internal data structures
0067     removeRows(0, rowCount());
0068 }
0069 
0070 QDir SnippetRepository::dataPath()
0071 {
0072     auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation));
0073     const auto &subdir = QLatin1String("ktexteditor_snippets/data/");
0074     bool success = dir.mkpath(dir.absoluteFilePath(subdir));
0075     Q_ASSERT(success);
0076     dir.setPath(dir.path() + QLatin1String("/") + subdir);
0077     return dir;
0078 }
0079 
0080 SnippetRepository *SnippetRepository::createRepoFromName(const QString &name)
0081 {
0082     QString cleanName = name;
0083     cleanName.replace(QLatin1Char('/'), QLatin1Char('-'));
0084 
0085     const auto &dir = dataPath();
0086     const auto &path = dir.absoluteFilePath(cleanName + QLatin1String(".xml"));
0087     //     qDebug() << "repo path:" << path << cleanName;
0088 
0089     SnippetRepository *repo = new SnippetRepository(path);
0090     repo->setText(name);
0091     repo->setCheckState(Qt::Checked);
0092     KUser user;
0093     repo->setAuthors(user.property(KUser::FullName).toString());
0094     SnippetStore::self()->appendRow(repo);
0095     return repo;
0096 }
0097 
0098 const QString &SnippetRepository::file() const
0099 {
0100     return m_file;
0101 }
0102 
0103 QString SnippetRepository::authors() const
0104 {
0105     return m_authors;
0106 }
0107 
0108 void SnippetRepository::setAuthors(const QString &authors)
0109 {
0110     m_authors = authors;
0111 }
0112 
0113 QStringList SnippetRepository::fileTypes() const
0114 {
0115     return m_filetypes;
0116 }
0117 
0118 void SnippetRepository::setFileTypes(const QStringList &filetypes)
0119 {
0120     if (filetypes.contains(QLatin1String("*"))) {
0121         m_filetypes.clear();
0122     } else {
0123         m_filetypes = filetypes;
0124     }
0125 }
0126 
0127 QString SnippetRepository::license() const
0128 {
0129     return m_license;
0130 }
0131 
0132 void SnippetRepository::setLicense(const QString &license)
0133 {
0134     m_license = license;
0135 }
0136 
0137 QString SnippetRepository::completionNamespace() const
0138 {
0139     return m_namespace;
0140 }
0141 
0142 void SnippetRepository::setCompletionNamespace(const QString &completionNamespace)
0143 {
0144     m_namespace = completionNamespace;
0145 }
0146 
0147 QString SnippetRepository::script() const
0148 {
0149     return m_script;
0150 }
0151 
0152 void SnippetRepository::setScript(const QString &script)
0153 {
0154     m_script = script;
0155 }
0156 
0157 void SnippetRepository::remove()
0158 {
0159     QFile::remove(m_file);
0160     setCheckState(Qt::Unchecked);
0161     model()->invisibleRootItem()->removeRow(row());
0162 }
0163 
0164 /// copied code from snippets_tng/lib/completionmodel.cpp
0165 ///@copyright 2009 Joseph Wenninger <jowenn@kde.org>
0166 static void addAndCreateElement(QDomDocument &doc, QDomElement &item, const QString &name, const QString &content)
0167 {
0168     QDomElement element = doc.createElement(name);
0169     element.appendChild(doc.createTextNode(content));
0170     item.appendChild(element);
0171 }
0172 
0173 void SnippetRepository::save()
0174 {
0175     //     qDebug() << "*** called";
0176     /// based on the code from snippets_tng/lib/completionmodel.cpp
0177     ///@copyright 2009 Joseph Wenninger <jowenn@kde.org>
0178     /*
0179     <snippets name="Testsnippets" filetype="*" authors="Joseph Wenninger" license="BSD" namespace="test::">
0180         <script>
0181             JavaScript
0182         </script>
0183         <item>
0184             <displayprefix>prefix</displayprefix>
0185             <match>test1</match>
0186             <displaypostfix>postfix</displaypostfix>
0187             <displayarguments>(param1, param2)</displayarguments>
0188             <fillin>This is a test</fillin>
0189         </item>
0190         <item>
0191             <match>testtemplate</match>
0192             <fillin>This is a test ${WHAT} template</fillin>
0193         </item>
0194     </snippets>
0195     */
0196     QDomDocument doc;
0197 
0198     QDomElement root = doc.createElement(QStringLiteral("snippets"));
0199     root.setAttribute(QStringLiteral("name"), text());
0200     root.setAttribute(QStringLiteral("filetypes"), m_filetypes.isEmpty() ? QStringLiteral("*") : m_filetypes.join(QLatin1Char(';')));
0201     root.setAttribute(QStringLiteral("authors"), m_authors);
0202     root.setAttribute(QStringLiteral("license"), m_license);
0203     root.setAttribute(QStringLiteral("namespace"), m_namespace);
0204 
0205     doc.appendChild(root);
0206 
0207     addAndCreateElement(doc, root, QStringLiteral("script"), m_script);
0208 
0209     for (int i = 0; i < rowCount(); ++i) {
0210         Snippet *snippet = Snippet::fromItem(child(i));
0211         if (!snippet) {
0212             continue;
0213         }
0214         QDomElement item = doc.createElement(QStringLiteral("item"));
0215         addAndCreateElement(doc, item, QStringLiteral("match"), snippet->text());
0216         addAndCreateElement(doc, item, QStringLiteral("fillin"), snippet->snippet());
0217         root.appendChild(item);
0218     }
0219     // KMessageBox::information(0,doc.toString());
0220     QFileInfo fi(m_file);
0221     QDir dir = dataPath();
0222     QString outname = dir.absoluteFilePath(fi.fileName());
0223 
0224     if (m_file != outname) {
0225         // there could be cases that new new name clashes with a global file, but I guess it is not that often.
0226         int i = 0;
0227         while (QFile::exists(outname)) {
0228             i++;
0229             outname = dir.absoluteFilePath(QString::number(i) + fi.fileName());
0230         }
0231         KMessageBox::information(QApplication::activeWindow(),
0232                                  i18n("You have edited a snippet repository file not located in your personal directory; as such, a copy of the original file "
0233                                       "has been created within your personal data directory."));
0234     }
0235 
0236     QFile outfile(outname);
0237     if (!outfile.open(QIODevice::WriteOnly)) {
0238         KMessageBox::error(nullptr, i18n("Output file '%1' could not be opened for writing", outname));
0239         return;
0240     }
0241     outfile.write(doc.toByteArray());
0242     outfile.close();
0243     m_file = outname;
0244 
0245     // save shortcuts
0246     KConfigGroup config = SnippetStore::self()->getConfig().group(QLatin1String("repository ") + m_file);
0247     for (int i = 0; i < rowCount(); ++i) {
0248         Snippet *snippet = Snippet::fromItem(child(i));
0249         if (!snippet) {
0250             continue;
0251         }
0252 
0253         QStringList shortcuts;
0254 
0255         const auto shortcutList = snippet->action()->shortcuts();
0256         for (const QKeySequence &keys : shortcutList) {
0257             shortcuts << keys.toString();
0258         }
0259 
0260         config.writeEntry(QLatin1String("shortcut ") + snippet->text(), shortcuts);
0261     }
0262     config.sync();
0263 }
0264 
0265 void SnippetRepository::parseFile()
0266 {
0267     /// based on the code from snippets_tng/lib/completionmodel.cpp
0268     ///@copyright 2009 Joseph Wenninger <jowenn@kde.org>
0269 
0270     QFile f(m_file);
0271 
0272     if (!f.open(QIODevice::ReadOnly)) {
0273         KMessageBox::error(QApplication::activeWindow(), i18n("Cannot open snippet repository %1.", m_file));
0274         return;
0275     }
0276 
0277     QDomDocument doc;
0278     QString errorMsg;
0279     int line, col;
0280     bool success = doc.setContent(&f, &errorMsg, &line, &col);
0281     f.close();
0282 
0283     if (!success) {
0284         KMessageBox::error(
0285             QApplication::activeWindow(),
0286             i18n("<qt>The error <b>%4</b><br /> has been detected in the file %1 at %2/%3</qt>", m_file, line, col, i18nc("QXml", errorMsg.toUtf8().data())));
0287         return;
0288     }
0289 
0290     // parse root item
0291     const QDomElement &docElement = doc.documentElement();
0292     if (docElement.tagName() != QLatin1String("snippets")) {
0293         KMessageBox::error(QApplication::activeWindow(), i18n("Invalid XML snippet file: %1", m_file));
0294         return;
0295     }
0296     setLicense(docElement.attribute(QStringLiteral("license")));
0297     setAuthors(docElement.attribute(QStringLiteral("authors")));
0298     setFileTypes(docElement.attribute(QStringLiteral("filetypes")).split(QLatin1Char(';'), Qt::SkipEmptyParts));
0299     setText(docElement.attribute(QStringLiteral("name")));
0300     setCompletionNamespace(docElement.attribute(QStringLiteral("namespace")));
0301 
0302     // load shortcuts
0303     KConfigGroup config = SnippetStore::self()->getConfig().group(QLatin1String("repository ") + m_file);
0304 
0305     // parse children, i.e. <item>'s
0306     const QDomNodeList &nodes = docElement.childNodes();
0307     for (int i = 0; i < nodes.size(); ++i) {
0308         const QDomNode &node = nodes.at(i);
0309         if (!node.isElement()) {
0310             continue;
0311         }
0312         const QDomElement &item = node.toElement();
0313         if (item.tagName() == QLatin1String("script")) {
0314             setScript(item.text());
0315         }
0316         if (item.tagName() != QLatin1String("item")) {
0317             continue;
0318         }
0319         Snippet *snippet = new Snippet;
0320         const QDomNodeList &children = node.childNodes();
0321         for (int j = 0; j < children.size(); ++j) {
0322             const QDomNode &childNode = children.at(j);
0323             if (!childNode.isElement()) {
0324                 continue;
0325             }
0326             const QDomElement &child = childNode.toElement();
0327             if (child.tagName() == QLatin1String("match")) {
0328                 snippet->setText(child.text());
0329             } else if (child.tagName() == QLatin1String("fillin")) {
0330                 snippet->setSnippet(child.text());
0331             }
0332         }
0333         // require at least a non-empty name and snippet
0334         if (snippet->text().isEmpty() || snippet->snippet().isEmpty()) {
0335             delete snippet;
0336             continue;
0337         } else {
0338             const QStringList shortcuts = config.readEntry(QLatin1String("shortcut ") + snippet->text(), QStringList());
0339             QList<QKeySequence> sequences;
0340             for (const QString &shortcut : shortcuts) {
0341                 sequences << QKeySequence::fromString(shortcut);
0342             }
0343 
0344             snippet->action()->setShortcuts(sequences);
0345 
0346             appendRow(snippet);
0347         }
0348     }
0349 }
0350 
0351 QVariant SnippetRepository::data(int role) const
0352 {
0353     if (role == Qt::ToolTipRole) {
0354         if (checkState() != Qt::Checked) {
0355             return i18n("Repository is disabled, the contained snippets will not be shown during code-completion.");
0356         }
0357         if (m_filetypes.isEmpty()) {
0358             return i18n("Applies to all filetypes");
0359         } else {
0360             return i18n("Applies to the following filetypes: %1", m_filetypes.join(QLatin1String(", ")));
0361         }
0362     } else if (role == Qt::ForegroundRole && checkState() != Qt::Checked) {
0363         /// TODO: make the selected items also "disalbed" so the toggle action is seen directly
0364         KColorScheme scheme(QPalette::Disabled, KColorScheme::View);
0365         QColor c = scheme.foreground(KColorScheme::NormalText).color();
0366         return QVariant(c);
0367     }
0368     return QStandardItem::data(role);
0369 }
0370 
0371 void SnippetRepository::setData(const QVariant &value, int role)
0372 {
0373     if (role == Qt::CheckStateRole) {
0374         const int state = value.toInt();
0375         if (state != checkState()) {
0376             KConfigGroup config = SnippetStore::self()->getConfig();
0377             QStringList currentlyEnabled = config.readEntry("enabledRepositories", QStringList());
0378             bool shouldSave = false;
0379             if (state == Qt::Checked && !currentlyEnabled.contains(m_file)) {
0380                 currentlyEnabled << m_file;
0381                 shouldSave = true;
0382             } else if (state == Qt::Unchecked && currentlyEnabled.contains(m_file)) {
0383                 currentlyEnabled.removeAll(m_file);
0384                 shouldSave = true;
0385             }
0386 
0387             if (shouldSave) {
0388                 config.writeEntry("enabledRepositories", currentlyEnabled);
0389                 config.sync();
0390             }
0391         }
0392     }
0393     QStandardItem::setData(value, role);
0394 }