1/*
2 SPDX-FileCopyrightText: 2009 Erlend Hamberg <ehamberg@gmail.com>
3 SPDX-FileCopyrightText: 2011 Svyatoslav Kuzmich <svatoslav1@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include <QDir>
9#include <QTimer>
10
11#include <KLocalizedString>
12#include <KTextEditor/Application>
13#include <KTextEditor/Document>
14#include <KTextEditor/Editor>
15#include <KTextEditor/MainWindow>
16#include <KTextEditor/View>
17
18#include <vimode/appcommands.h>
19
20using namespace KateVi;
21
22// BEGIN AppCommands
23AppCommands *AppCommands::m_instance = nullptr;
24
25AppCommands::AppCommands()
26 : KTextEditor::Command({QStringLiteral("q"), QStringLiteral("qa"), QStringLiteral("qall"), QStringLiteral("q!"), QStringLiteral("qa!"),
27 QStringLiteral("qall!"), QStringLiteral("w"), QStringLiteral("wq"), QStringLiteral("wa"), QStringLiteral("wqa"),
28 QStringLiteral("x"), QStringLiteral("xa"), QStringLiteral("new"), QStringLiteral("vnew"), QStringLiteral("e"),
29 QStringLiteral("edit"), QStringLiteral("enew"), QStringLiteral("sp"), QStringLiteral("split"), QStringLiteral("vs"),
30 QStringLiteral("vsplit"), QStringLiteral("only"), QStringLiteral("tabe"), QStringLiteral("tabedit"), QStringLiteral("tabnew"),
31 QStringLiteral("bd"), QStringLiteral("bdelete"), QStringLiteral("tabc"), QStringLiteral("tabclose"), QStringLiteral("clo"),
32 QStringLiteral("close")})
33 , re_write(QStringLiteral("^w(a)?$"))
34 , re_close(QStringLiteral("^bd(elete)?|tabc(lose)?$"))
35 , re_quit(QStringLiteral("^(w)?q(a|all)?(!)?$"))
36 , re_exit(QStringLiteral("^x(a)?$"))
37 , re_edit(QStringLiteral("^e(dit)?|tabe(dit)?|tabnew$"))
38 , re_tabedit(QStringLiteral("^tabe(dit)?|tabnew$"))
39 , re_new(QStringLiteral("^(v)?new$"))
40 , re_split(QStringLiteral("^sp(lit)?$"))
41 , re_vsplit(QStringLiteral("^vs(plit)?$"))
42 , re_vclose(QStringLiteral("^clo(se)?$"))
43 , re_only(QStringLiteral("^on(ly)?$"))
44{
45}
46
47AppCommands::~AppCommands()
48{
49 m_instance = nullptr;
50}
51
52bool AppCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &)
53{
54 QStringList args(cmd.split(sep: QRegularExpression(QStringLiteral("\\s+")), behavior: Qt::SkipEmptyParts));
55 QString command(args.takeFirst());
56
57 KTextEditor::MainWindow *mainWin = view->mainWindow();
58 KTextEditor::Application *app = KTextEditor::Editor::instance()->application();
59
60 QRegularExpressionMatch match;
61 if ((match = re_write.match(subject: command)).hasMatch()) { // TODO: handle writing to specific file
62 if (!match.captured(nth: 1).isEmpty()) { // [a]ll
63 const auto docs = app->documents();
64 for (KTextEditor::Document *doc : docs) {
65 doc->save();
66 }
67 msg = i18n("All documents written to disk");
68 } else {
69 view->document()->documentSave();
70 msg = i18n("Document written to disk");
71 }
72 }
73 // Other buffer commands are implemented by the KateFileTree plugin
74 else if ((match = re_close.match(subject: command)).hasMatch()) {
75 QTimer::singleShot(interval: 0, receiver: view, slot: [app, view]() {
76 app->closeDocument(document: view->document());
77 });
78 } else if ((match = re_quit.match(subject: command)).hasMatch()) {
79 const bool save = !match.captured(nth: 1).isEmpty(); // :[w]q
80 const bool allDocuments = !match.captured(nth: 2).isEmpty(); // :q[all]
81 const bool doNotPromptForSave = !match.captured(nth: 3).isEmpty(); // :q[!]
82
83 if (allDocuments) {
84 if (save) {
85 const auto docs = app->documents();
86 for (KTextEditor::Document *doc : docs) {
87 doc->save();
88 }
89 }
90
91 if (doNotPromptForSave) {
92 const auto docs = app->documents();
93 for (KTextEditor::Document *doc : docs) {
94 if (doc->isModified()) {
95 doc->setModified(false);
96 }
97 }
98 }
99
100 QTimer::singleShot(interval: 0, receiver: this, slot: [this, app]() {
101 closeDocuments(documents: app->documents());
102 });
103 } else {
104 if (save && view->document()->isModified()) {
105 view->document()->documentSave();
106 }
107
108 if (doNotPromptForSave) {
109 view->document()->setModified(false);
110 }
111
112 if (mainWin->views().size() > 1) {
113 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::closeCurrentView);
114 } else {
115 Q_ASSERT(app->documents().size() > 0);
116 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::closeCurrentDocument);
117 }
118 }
119 } else if ((match = re_exit.match(subject: command)).hasMatch()) {
120 if (!match.captured(nth: 1).isEmpty()) { // a[ll]
121 const auto docs = app->documents();
122 for (KTextEditor::Document *doc : docs) {
123 doc->save();
124 }
125 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::quit);
126 } else {
127 if (view->document()->isModified()) {
128 view->document()->documentSave();
129 }
130
131 if (app->documents().size() > 1) {
132 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::closeCurrentDocument);
133 } else {
134 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::quit);
135 }
136 }
137 } else if ((match = re_edit.match(subject: command)).hasMatch()) {
138 QString argument = args.join(sep: QLatin1Char(' '));
139 if (argument.isEmpty() || argument == QLatin1String("!")) {
140 if ((match = re_tabedit.match(subject: command)).hasMatch()) {
141 if (auto doc = app->openUrl(url: QUrl())) {
142 QTimer::singleShot(interval: 0, slot: [mainWin, doc]() {
143 mainWin->activateView(document: doc);
144 });
145 }
146 } else {
147 view->document()->documentReload();
148 }
149 } else {
150 QUrl base = view->document()->url();
151 QUrl url;
152 QUrl arg2path(argument);
153 if (base.isValid()) { // first try to use the same path as the current open document has
154 url =
155 QUrl(base.resolved(relative: arg2path)); // resolved handles the case where the args is a relative path, and is the same as using QUrl(args) elsewise
156 } else { // else use the cwd
157 url = QUrl(QUrl::fromLocalFile(localfile: QDir::currentPath() + QLatin1Char('/'))
158 .resolved(relative: arg2path)); // + "/" is needed because of https://lists.qt-project.org/pipermail/qt-interest-old/2011-May/033913.html
159 }
160
161 // either find existing document or just open it, openUrl will take care of non-existing files, too
162 KTextEditor::Document *doc = app->findUrl(url);
163 if (!doc) {
164 doc = app->openUrl(url);
165 }
166 if (doc) {
167 QTimer::singleShot(interval: 0, slot: [mainWin, doc]() {
168 mainWin->activateView(document: doc);
169 });
170 }
171 }
172 // splitView() orientations are reversed from the usual editor convention.
173 // 'vsplit' and 'vnew' use Qt::Horizontal to match vi and the Kate UI actions.
174 } else if ((match = re_new.match(subject: command)).hasMatch()) {
175 if (match.captured(nth: 1) == QLatin1String("v")) { // vertical split
176 mainWin->splitView(orientation: Qt::Horizontal);
177 } else { // horizontal split
178 mainWin->splitView(orientation: Qt::Vertical);
179 }
180 mainWin->openUrl(url: QUrl());
181 } else if (command == QLatin1String("enew")) {
182 mainWin->openUrl(url: QUrl());
183 } else if ((match = re_split.match(subject: command)).hasMatch()) {
184 mainWin->splitView(orientation: Qt::Vertical); // see above
185 } else if ((match = re_vsplit.match(subject: command)).hasMatch()) {
186 mainWin->splitView(orientation: Qt::Horizontal);
187 } else if ((match = re_vclose.match(subject: command)).hasMatch()) {
188 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::closeCurrentSplitView);
189 } else if ((match = re_only.match(subject: command)).hasMatch()) {
190 QTimer::singleShot(interval: 0, receiver: this, slot: &AppCommands::closeOtherSplitViews);
191 }
192
193 return true;
194}
195
196bool AppCommands::help(KTextEditor::View *view, const QString &cmd, QString &msg)
197{
198 Q_UNUSED(view);
199
200 if (re_write.match(subject: cmd).hasMatch()) {
201 msg = i18n(
202 "<p><b>w/wa &mdash; write document(s) to disk</b></p>"
203 "<p>Usage: <tt><b>w[a]</b></tt></p>"
204 "<p>Writes the current document(s) to disk. "
205 "It can be called in two ways:<br />"
206 " <tt>w</tt> &mdash; writes the current document to disk<br />"
207 " <tt>wa</tt> &mdash; writes all documents to disk.</p>"
208 "<p>If no file name is associated with the document, "
209 "a file dialog will be shown.</p>");
210 return true;
211 } else if (re_quit.match(subject: cmd).hasMatch()) {
212 msg = i18n(
213 "<p><b>q/qa/wq/wqa &mdash; [write and] quit</b></p>"
214 "<p>Usage: <tt><b>[w]q[a]</b></tt></p>"
215 "<p>Quits the application. If <tt>w</tt> is prepended, it also writes"
216 " the document(s) to disk. This command "
217 "can be called in several ways:<br />"
218 " <tt>q</tt> &mdash; closes the current view.<br />"
219 " <tt>qa</tt> &mdash; closes all views, effectively quitting the application.<br />"
220 " <tt>wq</tt> &mdash; writes the current document to disk and closes its view.<br />"
221 " <tt>wqa</tt> &mdash; writes all documents to disk and quits.</p>"
222 "<p>In all cases, if the view being closed is the last view, the application quits. "
223 "If no file name is associated with the document and it should be written to disk, "
224 "a file dialog will be shown.</p>");
225 return true;
226 } else if (re_exit.match(subject: cmd).hasMatch()) {
227 msg = i18n(
228 "<p><b>x/xa &mdash; write and quit</b></p>"
229 "<p>Usage: <tt><b>x[a]</b></tt></p>"
230 "<p>Saves document(s) and quits (e<b>x</b>its). This command "
231 "can be called in two ways:<br />"
232 " <tt>x</tt> &mdash; closes the current view.<br />"
233 " <tt>xa</tt> &mdash; closes all views, effectively quitting the application.</p>"
234 "<p>In all cases, if the view being closed is the last view, the application quits. "
235 "If no file name is associated with the document and it should be written to disk, "
236 "a file dialog will be shown.</p>"
237 "<p>Unlike the 'w' commands, this command only writes the document if it is modified."
238 "</p>");
239 return true;
240 } else if (re_split.match(subject: cmd).hasMatch()) {
241 msg = i18n(
242 "<p><b>sp,split&mdash; Split horizontally the current view into two</b></p>"
243 "<p>Usage: <tt><b>sp[lit]</b></tt></p>"
244 "<p>The result is two views on the same document.</p>");
245 return true;
246 } else if (re_vsplit.match(subject: cmd).hasMatch()) {
247 msg = i18n(
248 "<p><b>vs,vsplit&mdash; Split vertically the current view into two</b></p>"
249 "<p>Usage: <tt><b>vs[plit]</b></tt></p>"
250 "<p>The result is two views on the same document.</p>");
251 return true;
252 } else if (re_vclose.match(subject: cmd).hasMatch()) {
253 msg = i18n(
254 "<p><b>clo[se]&mdash; Close the current view</b></p>"
255 "<p>Usage: <tt><b>clo[se]</b></tt></p>"
256 "<p>After executing it, the current view will be closed.</p>");
257 return true;
258 } else if (re_new.match(subject: cmd).hasMatch()) {
259 msg = i18n(
260 "<p><b>[v]new &mdash; split view and create new document</b></p>"
261 "<p>Usage: <tt><b>[v]new</b></tt></p>"
262 "<p>Splits the current view and opens a new document in the new view."
263 " This command can be called in two ways:<br />"
264 " <tt>new</tt> &mdash; splits the view horizontally and opens a new document.<br />"
265 " <tt>vnew</tt> &mdash; splits the view vertically and opens a new document.<br />"
266 "</p>");
267 return true;
268 } else if (re_edit.match(subject: cmd).hasMatch()) {
269 msg = i18n(
270 "<p><b>e[dit] &mdash; reload current document</b></p>"
271 "<p>Usage: <tt><b>e[dit]</b></tt></p>"
272 "<p>Starts <b>e</b>diting the current document again. This is useful to re-edit"
273 " the current file, when it was modified on disk.</p>");
274 return true;
275 }
276
277 return false;
278}
279
280KTextEditor::View *AppCommands::findViewInDifferentSplitView(KTextEditor::MainWindow *window, KTextEditor::View *view)
281{
282 const auto views = window->views();
283 for (auto it : views) {
284 if (!window->viewsInSameSplitView(view1: it, view2: view)) {
285 return it;
286 }
287 }
288 return nullptr;
289}
290
291void AppCommands::closeDocuments(const QList<KTextEditor::Document *> &documents)
292{
293 auto app = KTextEditor::Editor::instance()->application();
294 QTimer::singleShot(interval: 0, receiver: app, slot: [app, documents]() {
295 app->closeDocuments(documents);
296 });
297}
298
299void AppCommands::closeCurrentDocument()
300{
301 auto app = KTextEditor::Editor::instance()->application();
302 auto mw = app->activeMainWindow();
303 if (auto view = mw->activeView()) {
304 auto doc = view->document();
305 QTimer::singleShot(interval: 0, receiver: doc, slot: [app, doc]() {
306 app->closeDocument(document: doc);
307 });
308 }
309}
310
311void AppCommands::closeCurrentView()
312{
313 auto app = KTextEditor::Editor::instance()->application();
314 auto mw = app->activeMainWindow();
315 if (auto view = mw->activeView()) {
316 mw->closeView(view);
317 }
318}
319
320void AppCommands::closeCurrentSplitView()
321{
322 auto app = KTextEditor::Editor::instance()->application();
323 auto mw = app->activeMainWindow();
324 if (auto view = mw->activeView()) {
325 mw->closeSplitView(view);
326 }
327}
328
329void AppCommands::closeOtherSplitViews()
330{
331 auto app = KTextEditor::Editor::instance()->application();
332 auto mw = app->activeMainWindow();
333 if (auto view = mw->activeView()) {
334 while (KTextEditor::View *viewToRemove = findViewInDifferentSplitView(window: mw, view)) {
335 mw->closeSplitView(view: viewToRemove);
336 }
337 }
338}
339
340void AppCommands::quit()
341{
342 KTextEditor::Editor::instance()->application()->quit();
343}
344
345// END AppCommands
346
347// BEGIN KateViBufferCommand
348BufferCommands *BufferCommands::m_instance = nullptr;
349
350BufferCommands::BufferCommands()
351 : KTextEditor::Command({QStringLiteral("ls"),
352 QStringLiteral("b"),
353 QStringLiteral("buffer"),
354 QStringLiteral("bn"),
355 QStringLiteral("bnext"),
356 QStringLiteral("bp"),
357 QStringLiteral("bprevious"),
358 QStringLiteral("tabn"),
359 QStringLiteral("tabnext"),
360 QStringLiteral("tabp"),
361 QStringLiteral("tabprevious"),
362 QStringLiteral("bf"),
363 QStringLiteral("bfirst"),
364 QStringLiteral("bl"),
365 QStringLiteral("blast"),
366 QStringLiteral("tabf"),
367 QStringLiteral("tabfirst"),
368 QStringLiteral("tabl"),
369 QStringLiteral("tablast")})
370{
371}
372
373BufferCommands::~BufferCommands()
374{
375 m_instance = nullptr;
376}
377
378bool BufferCommands::exec(KTextEditor::View *view, const QString &cmd, QString &, const KTextEditor::Range &)
379{
380 // create list of args
381 QStringList args(cmd.split(sep: QLatin1Char(' '), behavior: Qt::KeepEmptyParts));
382 QString command = args.takeFirst(); // same as cmd if split failed
383 QString argument = args.join(sep: QLatin1Char(' '));
384
385 if (command == QLatin1String("ls")) {
386 // TODO: open quickview
387 } else if (command == QLatin1String("b") || command == QLatin1String("buffer")) {
388 switchDocument(view, doc: argument);
389 } else if (command == QLatin1String("bp") || command == QLatin1String("bprevious")) {
390 prevBuffer(view);
391 } else if (command == QLatin1String("bn") || command == QLatin1String("bnext")) {
392 nextBuffer(view);
393 } else if (command == QLatin1String("bf") || command == QLatin1String("bfirst")) {
394 firstBuffer(view);
395 } else if (command == QLatin1String("bl") || command == QLatin1String("blast")) {
396 lastBuffer(view);
397 } else if (command == QLatin1String("tabn") || command == QLatin1String("tabnext")) {
398 nextTab(view);
399 } else if (command == QLatin1String("tabp") || command == QLatin1String("tabprevious")) {
400 prevTab(view);
401 } else if (command == QLatin1String("tabf") || command == QLatin1String("tabfirst")) {
402 firstTab(view);
403 } else if (command == QLatin1String("tabl") || command == QLatin1String("tablast")) {
404 lastTab(view);
405 }
406 return true;
407}
408
409void BufferCommands::switchDocument(KTextEditor::View *view, const QString &address)
410{
411 if (address.isEmpty()) {
412 // no argument: switch to the previous document
413 prevBuffer(view);
414 return;
415 }
416
417 const int idx = address.toInt();
418 QList<KTextEditor::Document *> docs = documents();
419
420 if (idx > 0 && idx <= docs.size()) {
421 // numerical argument: switch to the nth document
422 activateDocument(view, docs.at(i: idx - 1));
423 } else {
424 // string argument: switch to the given file
425 KTextEditor::Document *doc = nullptr;
426
427 for (KTextEditor::Document *it : docs) {
428 if (it->documentName() == address) {
429 doc = it;
430 break;
431 }
432 }
433
434 if (doc) {
435 activateDocument(view, doc);
436 }
437 }
438}
439
440void BufferCommands::prevBuffer(KTextEditor::View *view)
441{
442 const QList<KTextEditor::Document *> docs = documents();
443 const int idx = docs.indexOf(t: view->document());
444
445 if (idx > 0) {
446 activateDocument(view, docs.at(i: idx - 1));
447 } else if (!docs.isEmpty()) { // wrap
448 activateDocument(view, docs.last());
449 }
450}
451
452void BufferCommands::nextBuffer(KTextEditor::View *view)
453{
454 QList<KTextEditor::Document *> docs = documents();
455 const int idx = docs.indexOf(t: view->document());
456
457 if (idx + 1 < docs.size()) {
458 activateDocument(view, docs.at(i: idx + 1));
459 } else if (!docs.isEmpty()) { // wrap
460 activateDocument(view, docs.first());
461 }
462}
463
464void BufferCommands::firstBuffer(KTextEditor::View *view)
465{
466 auto docs = documents();
467 if (!docs.isEmpty()) {
468 activateDocument(view, documents().at(i: 0));
469 }
470}
471
472void BufferCommands::lastBuffer(KTextEditor::View *view)
473{
474 auto docs = documents();
475 if (!docs.isEmpty()) {
476 activateDocument(view, documents().last());
477 }
478}
479
480void BufferCommands::prevTab(KTextEditor::View *view)
481{
482 prevBuffer(view); // TODO: implement properly, when interface is added
483}
484
485void BufferCommands::nextTab(KTextEditor::View *view)
486{
487 nextBuffer(view); // TODO: implement properly, when interface is added
488}
489
490void BufferCommands::firstTab(KTextEditor::View *view)
491{
492 firstBuffer(view); // TODO: implement properly, when interface is added
493}
494
495void BufferCommands::lastTab(KTextEditor::View *view)
496{
497 lastBuffer(view); // TODO: implement properly, when interface is added
498}
499
500void BufferCommands::activateDocument(KTextEditor::View *view, KTextEditor::Document *doc)
501{
502 KTextEditor::MainWindow *mainWindow = view->mainWindow();
503 QTimer::singleShot(interval: 0, slot: [mainWindow, doc]() {
504 mainWindow->activateView(document: doc);
505 });
506}
507
508QList<KTextEditor::Document *> BufferCommands::documents()
509{
510 KTextEditor::Application *app = KTextEditor::Editor::instance()->application();
511 return app->documents();
512}
513
514bool BufferCommands::help(KTextEditor::View * /*view*/, const QString &cmd, QString &msg)
515{
516 if (cmd == QLatin1String("b") || cmd == QLatin1String("buffer")) {
517 msg = i18n(
518 "<p><b>b,buffer &mdash; Edit document N from the document list</b></p>"
519 "<p>Usage: <tt><b>b[uffer] [N]</b></tt></p>");
520 return true;
521 } else if (cmd == QLatin1String("bp") || cmd == QLatin1String("bprevious") || cmd == QLatin1String("tabp") || cmd == QLatin1String("tabprevious")) {
522 msg = i18n(
523 "<p><b>bp,bprev &mdash; previous buffer</b></p>"
524 "<p>Usage: <tt><b>bp[revious] [N]</b></tt></p>"
525 "<p>Goes to <b>[N]</b>th previous document (\"<b>b</b>uffer\") in document list. </p>"
526 "<p> <b>[N]</b> defaults to one. </p>"
527 "<p>Wraps around the start of the document list.</p>");
528 return true;
529 } else if (cmd == QLatin1String("bn") || cmd == QLatin1String("bnext") || cmd == QLatin1String("tabn") || cmd == QLatin1String("tabnext")) {
530 msg = i18n(
531 "<p><b>bn,bnext &mdash; switch to next document</b></p>"
532 "<p>Usage: <tt><b>bn[ext] [N]</b></tt></p>"
533 "<p>Goes to <b>[N]</b>th next document (\"<b>b</b>uffer\") in document list."
534 "<b>[N]</b> defaults to one. </p>"
535 "<p>Wraps around the end of the document list.</p>");
536 return true;
537 } else if (cmd == QLatin1String("bf") || cmd == QLatin1String("bfirst") || cmd == QLatin1String("tabf") || cmd == QLatin1String("tabfirst")) {
538 msg = i18n(
539 "<p><b>bf,bfirst &mdash; first document</b></p>"
540 "<p>Usage: <tt><b>bf[irst]</b></tt></p>"
541 "<p>Goes to the <b>f</b>irst document (\"<b>b</b>uffer\") in document list.</p>");
542 return true;
543 } else if (cmd == QLatin1String("bl") || cmd == QLatin1String("blast") || cmd == QLatin1String("tabl") || cmd == QLatin1String("tablast")) {
544 msg = i18n(
545 "<p><b>bl,blast &mdash; last document</b></p>"
546 "<p>Usage: <tt><b>bl[ast]</b></tt></p>"
547 "<p>Goes to the <b>l</b>ast document (\"<b>b</b>uffer\") in document list.</p>");
548 return true;
549 } else if (cmd == QLatin1String("ls")) {
550 msg = i18n(
551 "<p><b>ls</b></p>"
552 "<p>list current buffers<p>");
553 }
554
555 return false;
556}
557// END KateViBufferCommand
558

source code of ktexteditor/src/vimode/appcommands.cpp