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 }