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