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