1 /*
2     SPDX-FileCopyrightText: 2002, 2003, 2004 Anders Lund <anders.lund@lund.tdcadsl.dk>
3     SPDX-FileCopyrightText: 2002 John Firebaugh <jfirebaugh@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "katebookmarks.h"
9 
10 #include "kateabstractinputmode.h"
11 #include "katedocument.h"
12 #include "kateview.h"
13 
14 #include <KActionCollection>
15 #include <KActionMenu>
16 #include <KGuiItem>
17 #include <KLocalizedString>
18 #include <KStringHandler>
19 #include <KToggleAction>
20 #include <KXMLGUIClient>
21 #include <KXMLGUIFactory>
22 
23 #include <QEvent>
24 #include <QRegularExpression>
25 #include <QVector>
26 
27 namespace KTextEditor
28 {
29 class Document;
30 }
31 
KateBookmarks(KTextEditor::ViewPrivate * view,Sorting sort)32 KateBookmarks::KateBookmarks(KTextEditor::ViewPrivate *view, Sorting sort)
33     : QObject(view)
34     , m_view(view)
35     , m_bookmarkClear(nullptr)
36     , m_sorting(sort)
37 {
38     setObjectName(QStringLiteral("kate bookmarks"));
39     connect(view->doc(), &KTextEditor::DocumentPrivate::marksChanged, this, &KateBookmarks::marksChanged);
40     _tries = 0;
41     m_bookmarksMenu = nullptr;
42 }
43 
44 KateBookmarks::~KateBookmarks() = default;
45 
createActions(KActionCollection * ac)46 void KateBookmarks::createActions(KActionCollection *ac)
47 {
48     m_bookmarkToggle = new KToggleAction(i18n("Set &Bookmark"), this);
49     ac->addAction(QStringLiteral("bookmarks_toggle"), m_bookmarkToggle);
50     m_bookmarkToggle->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-new")));
51     ac->setDefaultShortcut(m_bookmarkToggle, Qt::CTRL + Qt::Key_B);
52     m_bookmarkToggle->setWhatsThis(i18n("If a line has no bookmark then add one, otherwise remove it."));
53     connect(m_bookmarkToggle, &QAction::triggered, this, &KateBookmarks::toggleBookmark);
54 
55     m_bookmarkClear = new QAction(i18n("Clear &All Bookmarks"), this);
56     ac->addAction(QStringLiteral("bookmarks_clear"), m_bookmarkClear);
57     m_bookmarkClear->setIcon(QIcon::fromTheme(QStringLiteral("bookmark-remove")));
58     m_bookmarkClear->setWhatsThis(i18n("Remove all bookmarks of the current document."));
59     connect(m_bookmarkClear, &QAction::triggered, this, &KateBookmarks::clearBookmarks);
60 
61     m_goNext = new QAction(i18n("Next Bookmark"), this);
62     ac->addAction(QStringLiteral("bookmarks_next"), m_goNext);
63     m_goNext->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
64     ac->setDefaultShortcut(m_goNext, Qt::ALT + Qt::Key_PageDown);
65     m_goNext->setWhatsThis(i18n("Go to the next bookmark."));
66     connect(m_goNext, &QAction::triggered, this, &KateBookmarks::goNext);
67 
68     m_goPrevious = new QAction(i18n("Previous Bookmark"), this);
69     ac->addAction(QStringLiteral("bookmarks_previous"), m_goPrevious);
70     m_goPrevious->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search")));
71     ac->setDefaultShortcut(m_goPrevious, Qt::ALT + Qt::Key_PageUp);
72     m_goPrevious->setWhatsThis(i18n("Go to the previous bookmark."));
73     connect(m_goPrevious, &QAction::triggered, this, &KateBookmarks::goPrevious);
74 
75     KActionMenu *actionMenu = new KActionMenu(i18n("&Bookmarks"), this);
76     actionMenu->setPopupMode(QToolButton::InstantPopup);
77     ac->addAction(QStringLiteral("bookmarks"), actionMenu);
78     m_bookmarksMenu = actionMenu->menu();
79 
80     connect(m_bookmarksMenu, &QMenu::aboutToShow, this, &KateBookmarks::bookmarkMenuAboutToShow);
81 
82     marksChanged();
83 
84     // Always want the actions with shortcuts plugged into something so their shortcuts can work
85     m_view->addAction(m_bookmarkToggle);
86     m_view->addAction(m_bookmarkClear);
87     m_view->addAction(m_goNext);
88     m_view->addAction(m_goPrevious);
89 }
90 
toggleBookmark()91 void KateBookmarks::toggleBookmark()
92 {
93     uint mark = m_view->doc()->mark(m_view->cursorPosition().line());
94     if (mark & KTextEditor::MarkInterface::markType01) {
95         m_view->doc()->removeMark(m_view->cursorPosition().line(), KTextEditor::MarkInterface::markType01);
96     } else {
97         m_view->doc()->addMark(m_view->cursorPosition().line(), KTextEditor::MarkInterface::markType01);
98     }
99 }
100 
clearBookmarks()101 void KateBookmarks::clearBookmarks()
102 {
103     // work on a COPY of the hash, the removing will modify it otherwise!
104     const auto hash = m_view->doc()->marks();
105     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
106         m_view->doc()->removeMark(it.value()->line, KTextEditor::MarkInterface::markType01);
107     }
108 }
109 
insertBookmarks(QMenu & menu)110 void KateBookmarks::insertBookmarks(QMenu &menu)
111 {
112     const int line = m_view->cursorPosition().line();
113     static const QRegularExpression re(QStringLiteral("&(?!&)"));
114     int next = -1; // -1 means next bookmark doesn't exist
115     int prev = -1; // -1 means previous bookmark doesn't exist
116 
117     // reference ok, not modified
118     const auto &hash = m_view->doc()->marks();
119     if (hash.isEmpty()) {
120         return;
121     }
122 
123     QVector<int> bookmarkLineArray; // Array of line numbers which have bookmarks
124 
125     // Find line numbers where bookmarks are set & store those line numbers in bookmarkLineArray
126     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
127         if (it.value()->type & KTextEditor::MarkInterface::markType01) {
128             bookmarkLineArray.append(it.value()->line);
129         }
130     }
131 
132     if (m_sorting == Position) {
133         std::sort(bookmarkLineArray.begin(), bookmarkLineArray.end());
134     }
135 
136     QAction *firstNewAction = menu.addSeparator();
137     // Consider each line with a bookmark one at a time
138     for (int i = 0; i < bookmarkLineArray.size(); ++i) {
139         const int lineNo = bookmarkLineArray.at(i);
140         // Get text in this particular line in a QString
141         QFontMetrics fontMetrics(menu.fontMetrics());
142         QString bText = fontMetrics.elidedText(m_view->doc()->line(lineNo), Qt::ElideRight, fontMetrics.maxWidth() * 32);
143         bText.replace(re, QStringLiteral("&&")); // kill undesired accellerators!
144         bText.replace(QLatin1Char('\t'), QLatin1Char(' ')); // kill tabs, as they are interpreted as shortcuts
145 
146         QAction *before = nullptr;
147         if (m_sorting == Position) {
148             // 3 actions already present
149             if (menu.actions().size() <= i + 3) {
150                 before = nullptr;
151             } else {
152                 before = menu.actions().at(i + 3);
153             }
154         }
155 
156         const QString actionText(QStringLiteral("%1  %2  - \"%3\"").arg(QString::number(lineNo + 1), m_view->currentInputMode()->bookmarkLabel(lineNo), bText));
157         // Adding action for this bookmark in menu
158         if (before) {
159             QAction *a = new QAction(actionText, &menu);
160             menu.insertAction(before, a);
161             connect(a, &QAction::triggered, this, [this, lineNo]() {
162                 gotoLine(lineNo);
163             });
164 
165             if (!firstNewAction) {
166                 firstNewAction = a;
167             }
168         } else {
169             menu.addAction(actionText, this, [this, lineNo]() {
170                 gotoLine(lineNo);
171             });
172         }
173 
174         // Find the line number of previous & next bookmark (if present) in relation to the cursor
175         if (lineNo < line) {
176             if (prev == -1 || prev < lineNo) {
177                 prev = lineNo;
178             }
179         } else if (lineNo > line) {
180             if (next == -1 || next > lineNo) {
181                 next = lineNo;
182             }
183         }
184     }
185 
186     if (next != -1) {
187         // Insert action for next bookmark
188         m_goNext->setText(i18n("&Next: %1 - \"%2\"", next + 1, KStringHandler::rsqueeze(m_view->doc()->line(next), 24)));
189         menu.insertAction(firstNewAction, m_goNext);
190         firstNewAction = m_goNext;
191     }
192     if (prev != -1) {
193         // Insert action for previous bookmark
194         m_goPrevious->setText(i18n("&Previous: %1 - \"%2\"", prev + 1, KStringHandler::rsqueeze(m_view->doc()->line(prev), 24)));
195         menu.insertAction(firstNewAction, m_goPrevious);
196         firstNewAction = m_goPrevious;
197     }
198 
199     if (next != -1 || prev != -1) {
200         menu.insertSeparator(firstNewAction);
201     }
202 }
203 
gotoLine(int line)204 void KateBookmarks::gotoLine(int line)
205 {
206     m_view->setCursorPosition(KTextEditor::Cursor(line, 0));
207 }
208 
bookmarkMenuAboutToShow()209 void KateBookmarks::bookmarkMenuAboutToShow()
210 {
211     m_bookmarksMenu->clear();
212     m_bookmarkToggle->setChecked(m_view->doc()->mark(m_view->cursorPosition().line()) & KTextEditor::MarkInterface::markType01);
213     m_bookmarksMenu->addAction(m_bookmarkToggle);
214     m_bookmarksMenu->addAction(m_bookmarkClear);
215 
216     m_goNext->setText(i18n("Next Bookmark"));
217     m_goPrevious->setText(i18n("Previous Bookmark"));
218 
219     insertBookmarks(*m_bookmarksMenu);
220 }
221 
goNext()222 void KateBookmarks::goNext()
223 {
224     // reference ok, not modified
225     const auto &hash = m_view->doc()->marks();
226     if (hash.isEmpty()) {
227         return;
228     }
229 
230     int line = m_view->cursorPosition().line();
231     int found = -1;
232 
233     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
234         const int markLine = it.value()->line;
235         if (markLine > line && (found == -1 || found > markLine)) {
236             found = markLine;
237         }
238     }
239 
240     if (found != -1) {
241         gotoLine(found);
242     }
243 }
244 
goPrevious()245 void KateBookmarks::goPrevious()
246 {
247     // reference ok, not modified
248     const auto &hash = m_view->doc()->marks();
249     if (hash.isEmpty()) {
250         return;
251     }
252 
253     int line = m_view->cursorPosition().line();
254     int found = -1;
255 
256     for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
257         const int markLine = it.value()->line;
258         if (markLine < line && (found == -1 || found < markLine)) {
259             found = markLine;
260         }
261     }
262 
263     if (found != -1) {
264         gotoLine(found);
265     }
266 }
267 
marksChanged()268 void KateBookmarks::marksChanged()
269 {
270     if (m_bookmarkClear) {
271         m_bookmarkClear->setEnabled(!m_view->doc()->marks().isEmpty());
272     }
273 }
274