1 /*
2 * Copyright (C) Pedram Pourang (aka Tsu Jan) 2014-2019 <tsujan2000@gmail.com>
3 *
4 * FeatherPad is free software: you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License as published by the
6 * Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * FeatherPad is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12 * See the GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * @license GPL-3.0+ <https://spdx.org/licenses/GPL-3.0+.html>
18 */
19
20 #include "singleton.h"
21 #include "ui_fp.h"
22 #include <QMimeDatabase>
23 #include <QFileInfo>
24
25 namespace FeatherPad {
26
getMimeType(const QFileInfo & fInfo)27 static QMimeType getMimeType (const QFileInfo &fInfo)
28 {
29 QMimeDatabase mimeDatabase;
30 return mimeDatabase.mimeTypeForFile (fInfo);
31 }
32 /*************************/
toggleSyntaxHighlighting()33 void FPwin::toggleSyntaxHighlighting()
34 {
35 int count = ui->tabWidget->count();
36 if (count == 0) return;
37
38 bool enableSH = ui->actionSyntax->isChecked();
39 if (enableSH)
40 makeBusy(); // it may take a while with huge texts
41
42 for (int i = 0; i < count; ++i)
43 {
44 TextEdit *textEdit = qobject_cast< TabPage *>(ui->tabWidget->widget (i))->textEdit();
45 syntaxHighlighting (textEdit, enableSH, textEdit->getLang());
46 }
47
48 if (TabPage *tabPage = qobject_cast< TabPage *>(ui->tabWidget->currentWidget()))
49 updateLangBtn (tabPage->textEdit());
50
51 if (enableSH)
52 QTimer::singleShot (0, this, &FPwin::unbusy);
53 }
54 /*************************/
55 // Falls back to "url".
setProgLang(TextEdit * textEdit)56 void FPwin::setProgLang (TextEdit *textEdit)
57 {
58 if (textEdit == nullptr) return;
59
60 QString fname = textEdit->getFileName();
61 if (fname.isEmpty()) return;
62
63 /* examine the (final) target if existing */
64 QFileInfo fInfo (fname);
65 if (fInfo.isSymLink())
66 {
67 const QString finalTarget = fInfo.canonicalFilePath();
68 if (!finalTarget.isEmpty())
69 fname = finalTarget;
70 else
71 fname = fInfo.symLinkTarget();
72 }
73
74 if (fname.endsWith (".sub"))
75 return; // "url" is the default for TextEdit
76
77 QString progLan;
78
79 /* first check some endings */
80 QString baseName = fname.section ('/', -1);
81 QRegularExpressionMatch match = QRegularExpression ("\\A(?:[^/]*\\.[^/\\.]+)\\z").match (baseName);
82 if (match.hasMatch())
83 {
84 if (fname.endsWith (".cpp") || fname.endsWith (".h"))
85 progLan = "cpp";
86 else if (fname.endsWith (".c"))
87 progLan = "c";
88 else if (fname.endsWith (".sh") || fname.endsWith (".bashrc") || fname.endsWith (".rules")
89 || baseName == ".bash_profile" || baseName == ".bash_functions"
90 || baseName == ".xprofile" || baseName == ".profile"
91 || baseName == ".bash_aliases" || baseName == ".mkshrc"
92 || baseName == ".zprofile" || baseName == ".zlogin"
93 || baseName == ".zshrc" || baseName == ".zshenv")
94 progLan = "sh";
95 else if (fname.endsWith (".rb"))
96 progLan = "ruby";
97 else if (fname.endsWith (".lua") || fname.endsWith (".nelua"))
98 progLan = "lua";
99 else if (fname.endsWith (".py"))
100 progLan = "python";
101 else if (fname.endsWith (".pl"))
102 progLan = "perl";
103 else if (fname.endsWith (".pro") || fname.endsWith (".pri"))
104 progLan = "qmake";
105 else if (fname.endsWith (".tr") || fname.endsWith (".t") || fname.endsWith (".roff"))
106 progLan = "troff";
107 else if (fname.endsWith (".tex") || fname.endsWith (".ltx") || fname.endsWith (".latex") || fname.endsWith (".lyx"))
108 progLan = "LaTeX";
109 else if (fname.endsWith (".xml", Qt::CaseInsensitive) || fname.endsWith (".svg", Qt::CaseInsensitive) || fname.endsWith (".qrc")
110 || fname.endsWith (".meta4", Qt::CaseInsensitive) || fname.endsWith (".metalink", Qt::CaseInsensitive)
111 || fname.endsWith (".rdf") || fname.endsWith (".docbook") || fname.endsWith (".fnx")
112 || fname.endsWith (".ts") || fname.endsWith (".menu") || fname.endsWith (".kml", Qt::CaseInsensitive)
113 || fname.endsWith (".xspf", Qt::CaseInsensitive) || fname.endsWith (".asx", Qt::CaseInsensitive)
114 || fname.endsWith (".nfo")/* || fname.endsWith (".ui") || fname.endsWith (".xul")*/)
115 progLan = "xml";
116 else if (fname.endsWith (".css") || fname.endsWith (".qss"))
117 progLan = "css";
118 else if (fname.endsWith (".scss"))
119 progLan = "scss";
120 else if (fname.endsWith (".p") || fname.endsWith (".pas"))
121 progLan = "pascal";
122 else if (fname.endsWith (".desktop") || fname.endsWith (".desktop.in") || fname.endsWith (".directory"))
123 progLan = "desktop";
124 else if (fname.endsWith (".kvconfig")
125 || fname.endsWith (".service") || fname.endsWith (".mount") || fname.endsWith (".timer") // systemd related
126 || baseName == "sources.list" || baseName == "sources.list.save"
127 || baseName == "mimeinfo.cache" || baseName == "defaults.list"
128 || baseName == "mimeapps.list" || baseName.endsWith ("-mimeapps.list")
129 || fname.endsWith (".pls", Qt::CaseInsensitive))
130 progLan = "config";
131 else if (fname.endsWith (".js") || fname.endsWith (".hx"))
132 progLan = "javascript";
133 else if (fname.endsWith (".java"))
134 progLan = "java";
135 else if (fname.endsWith (".json"))
136 progLan = "json";
137 else if (fname.endsWith (".qml"))
138 progLan = "qml";
139 else if (fname.endsWith (".log", Qt::CaseInsensitive))
140 progLan = "log";
141 else if (fname.endsWith (".php"))
142 progLan = "php";
143 else if (fname.endsWith (".diff") || fname.endsWith (".patch"))
144 progLan = "diff";
145 else if (fname.endsWith (".srt"))
146 progLan = "srt";
147 else if (fname.endsWith (".theme"))
148 progLan = "theme";
149 else if (fname.endsWith (".fountain"))
150 progLan = "fountain";
151 else if (fname.endsWith (".yml") || fname.endsWith (".yaml"))
152 progLan = "yaml";
153 else if (fname.endsWith (".m3u", Qt::CaseInsensitive))
154 progLan = "m3u";
155 else if (fname.endsWith (".htm", Qt::CaseInsensitive) || fname.endsWith (".html", Qt::CaseInsensitive))
156 progLan = "html";
157 else if (fname.endsWith (".markdown") || fname.endsWith (".md") || fname.endsWith (".mkd"))
158 progLan = "markdown";
159 else if (fname.endsWith (".rst"))
160 progLan = "reST";
161 else if (fname.endsWith (".dart"))
162 progLan = "dart";
163 else if (fname.endsWith (".go"))
164 progLan = "go";
165 else if (baseName.startsWith ("makefile.", Qt::CaseInsensitive) && !baseName.endsWith (".txt", Qt::CaseInsensitive))
166 progLan = "makefile";
167 else if (baseName.compare ("CMakeLists.txt", Qt::CaseInsensitive) == 0)
168 progLan = "cmake";
169 }
170 else if (baseName == "PKGBUILD" || baseName == "fstab")
171 progLan = "sh";
172 /* makefile is an exception */
173 else if (baseName.compare ("makefile", Qt::CaseInsensitive) == 0)
174 progLan = "makefile";
175 else if (baseName.compare ("changelog", Qt::CaseInsensitive) == 0)
176 progLan = "changelog";
177 else if (baseName == "gtkrc")
178 progLan = "gtkrc";
179 else if (baseName == "control")
180 progLan = "deb";
181 else if (baseName == "mirrorlist")
182 progLan = "config";
183
184 if (progLan.isEmpty()) // now, check the mime type
185 {
186 if (!fInfo.exists())
187 progLan = "url"; // fall back to the default language
188 else
189 {
190 QMimeType mimeType = fInfo.isSymLink() ? getMimeType (QFileInfo (fname)) : getMimeType (fInfo);
191 const QString mime = mimeType.name();
192 QString parentMime;
193 auto parents = mimeType.parentMimeTypes();
194 if (!parents.isEmpty())
195 parentMime = parents.at (0);
196
197 if (mime == "text/x-c++" || mime == "text/x-c++src" || mime == "text/x-c++hdr" || mime == "text/x-chdr")
198 progLan = "cpp";
199 else if (mime == "text/x-c" || mime == "text/x-csrc")
200 progLan = "c";
201 else if (mime == "application/x-shellscript" || mime == "text/x-shellscript")
202 progLan = "sh";
203 else if (mime == "application/x-ruby")
204 progLan = "ruby";
205 else if (mime == "text/x-lua")
206 progLan = "lua";
207 else if (mime.startsWith ("text/x-python")) // it may be "text/x-python3"
208 progLan = "python";
209 else if (mime == "application/x-perl")
210 progLan = "perl";
211 else if (mime == "text/x-makefile")
212 progLan = "makefile";
213 else if (mime == "text/x-cmake")
214 progLan = "cmake";
215 else if (mime == "application/vnd.nokia.qt.qmakeprofile")
216 progLan = "qmake";
217 else if (mime == "text/troff")
218 progLan = "troff";
219 else if (mime == "text/x-tex" || mime == "application/x-lyx")
220 progLan = "LaTeX";
221 else if (mime == "text/html" || parentMime == "text/html" || mime == "application/xhtml+xml") // should come before xml check
222 progLan = "html";
223 else if (mime == "application/xml" || parentMime == "application/xml"
224 || mime == "text/feathernotes-fnx" || mime == "audio/x-ms-asx" || mime == "text/x-nfo")
225 progLan = "xml";
226 else if (mime == "text/css")
227 progLan = "css";
228 else if (mime == "text/x-scss")
229 progLan = "scss";
230 else if (mime == "text/x-pascal")
231 progLan = "pascal";
232 else if (mime == "text/x-changelog")
233 progLan = "changelog";
234 else if (mime == "application/x-desktop")
235 progLan = "desktop";
236 else if (mime == "audio/x-scpls" || mime == "application/vnd.kde.kcfgc")
237 progLan = "config";
238 else if (mime == "application/javascript")
239 progLan = "javascript";
240 else if (mime == "text/x-java")
241 progLan = "java";
242 else if (mime == "application/json")
243 progLan = "json";
244 else if (mime == "text/x-qml")
245 progLan = "qml";
246 else if (mime == "text/x-log")
247 progLan = "log";
248 else if (mime == "application/x-php" || mime == "text/x-php")
249 progLan = "php";
250 else if (mime == "application/x-theme")
251 progLan = "theme";
252 else if (mime == "application/x-yaml")
253 progLan = "yaml";
254 else if (mime == "text/x-diff" || mime == "text/x-patch")
255 progLan = "diff";
256 else if (mime == "text/markdown")
257 progLan = "markdown";
258 else if (mime == "audio/x-mpegurl" || mime == "application/vnd.apple.mpegurl")
259 progLan = "m3u";
260 else if (mime == "text/x-go")
261 progLan = "go";
262 else if (fname.endsWith (".conf") || fname.endsWith (".ini"))
263 progLan = "config"; // only if the mime type isn't found
264 else // fall back to the default language
265 progLan = "url";
266 }
267 }
268
269 textEdit->setProg (progLan);
270 }
271 /*************************/
syntaxHighlighting(TextEdit * textEdit,bool highlight,const QString & lang)272 void FPwin::syntaxHighlighting (TextEdit *textEdit, bool highlight, const QString& lang)
273 {
274 if (textEdit == nullptr
275 || textEdit->isUneditable()) // has huge lines or isn't a text
276 {
277 return;
278 }
279
280 if (highlight)
281 {
282 QString progLan = lang; // first try the enforced language
283 if (progLan.isEmpty())
284 progLan = textEdit->getProg();
285 if (progLan == "help" // used for marking the help doc
286 || progLan.isEmpty()) // impossible; just a precaution
287 {
288 return;
289 }
290
291 Config config = static_cast<FPsingleton*>(qApp)->getConfig();
292 if (textEdit->getSize() > config.getMaxSHSize()*1024*1024)
293 {
294 QTimer::singleShot (100, textEdit, [=]() {
295 if (TabPage *tabPage = qobject_cast<TabPage*>(ui->tabWidget->currentWidget()))
296 {
297 if (tabPage->textEdit() == textEdit)
298 showWarningBar ("<center><b><big>" + tr ("The size limit for syntax highlighting is exceeded.") + "</big></b></center>");
299 }
300 });
301 return;
302 }
303
304 if (!qobject_cast< Highlighter *>(textEdit->getHighlighter()))
305 {
306 QPoint Point (0, 0);
307 QTextCursor start = textEdit->cursorForPosition (Point);
308 Point = QPoint (textEdit->geometry().width(), textEdit->geometry().height());
309 QTextCursor end = textEdit->cursorForPosition (Point);
310
311 textEdit->setDrawIndetLines (config.getShowWhiteSpace());
312 textEdit->setVLineDistance (config.getVLineDistance());
313 Highlighter *highlighter = new Highlighter (textEdit->document(), progLan, start, end,
314 textEdit->hasDarkScheme(),
315 config.getShowWhiteSpace(),
316 config.getShowEndings(),
317 config.getWhiteSpaceValue(),
318 config.customSyntaxColors().isEmpty()
319 ? textEdit->hasDarkScheme() ? config.darkSyntaxColors()
320 : config.lightSyntaxColors()
321 : config.customSyntaxColors());
322 textEdit->setHighlighter (highlighter);
323 }
324 /* if the highlighter is created just now, it's necessary
325 to wait until the text is completely loaded */
326 QTimer::singleShot (0, textEdit, [this, textEdit]() {
327 if (textEdit->isVisible())
328 {
329 formatTextRect(); // the text may be scrolled immediately after syntax highlighting (when reloading)
330 matchBrackets(); // in case the cursor is beside a bracket when the text is loaded
331 }
332 connect (textEdit, &TextEdit::updateBracketMatching, this, &FPwin::matchBrackets);
333 /* visible text may change on block removal */
334 connect (textEdit, &QPlainTextEdit::blockCountChanged, this, &FPwin::formatOnBlockChange);
335 connect (textEdit, &TextEdit::updateRect, this, &FPwin::formatTextRect);
336 connect (textEdit, &TextEdit::resized, this, &FPwin::formatTextRect);
337 /* this is needed when the whole visible text is pasted */
338 connect (textEdit->document(), &QTextDocument::contentsChange, this, &FPwin::formatOnTextChange);
339 });
340 }
341 else if (Highlighter *highlighter = qobject_cast< Highlighter *>(textEdit->getHighlighter()))
342 {
343 disconnect (textEdit->document(), &QTextDocument::contentsChange, this, &FPwin::formatOnTextChange);
344 disconnect (textEdit, &TextEdit::resized, this, &FPwin::formatTextRect);
345 disconnect (textEdit, &TextEdit::updateRect, this, &FPwin::formatTextRect);
346 disconnect (textEdit, &QPlainTextEdit::blockCountChanged, this, &FPwin::formatOnBlockChange);
347 disconnect (textEdit, &TextEdit::updateBracketMatching, this, &FPwin::matchBrackets);
348
349 /* remove bracket highlights */
350 QList<QTextEdit::ExtraSelection> es = textEdit->extraSelections();
351 int n = textEdit->getRedSel().count();
352 while (n > 0 && !es.isEmpty())
353 {
354 es.removeLast();
355 --n;
356 }
357 textEdit->setRedSel (QList<QTextEdit::ExtraSelection>());
358 textEdit->setExtraSelections (es);
359
360 textEdit->setDrawIndetLines (false);
361 textEdit->setVLineDistance (0);
362
363 textEdit->setHighlighter (nullptr);
364 delete highlighter; highlighter = nullptr;
365 }
366 }
367 /*************************/
formatOnTextChange(int,int charsRemoved,int charsAdded) const368 void FPwin::formatOnTextChange (int /*position*/, int charsRemoved, int charsAdded) const
369 {
370 if (charsRemoved > 0 || charsAdded > 0)
371 {
372 /* wait until the document's layout manager is notified about the change;
373 otherwise, the end cursor might be out of range in formatTextRect() */
374 QTimer::singleShot (0, this, &FPwin::formatTextRect);
375 }
376 }
377 /*************************/
formatOnBlockChange(int) const378 void FPwin::formatOnBlockChange (int/* newBlockCount*/) const
379 {
380 formatTextRect();
381 }
382 /*************************/
formatTextRect() const383 void FPwin::formatTextRect() const
384 {
385 /* It's supposed that this function is called for the current tab.
386 That isn't always the case and won't do any harm if it isn't. */
387 if (TabPage *tabPage = qobject_cast<TabPage*>(ui->tabWidget->currentWidget()))
388 {
389 TextEdit *textEdit = tabPage->textEdit();
390 Highlighter *highlighter = qobject_cast< Highlighter *>(textEdit->getHighlighter());
391 if (highlighter == nullptr) return;
392
393 QPoint Point (0, 0);
394 QTextCursor start = textEdit->cursorForPosition (Point);
395 Point = QPoint (textEdit->width(), textEdit->height());
396 QTextCursor end = textEdit->cursorForPosition (Point);
397
398 highlighter->setLimit (start, end);
399 QTextBlock block = start.block();
400 while (block.isValid() && block.blockNumber() <= end.blockNumber())
401 {
402 if (TextBlockData *data = static_cast<TextBlockData *>(block.userData()))
403 {
404 if (!data->isHighlighted()) // isn't highlighted (completely)
405 highlighter->rehighlightBlock (block);
406 }
407 block = block.next();
408 }
409 }
410 }
411
412 }
413