[education/rkward] rkward: Start adding some polish

Thomas Friedrichsmeier null at kde.org
Mon May 26 15:06:38 BST 2025


Git commit 3dd8e85f3e4585f475397abd8679a5d52cbc6b15 by Thomas Friedrichsmeier.
Committed on 26/05/2025 at 14:06.
Pushed by tfry into branch 'master'.

Start adding some polish

M  +6    -0    rkward/misc/rkparsedscript.h
M  +1    -0    rkward/pages/rkward_for_r_users.rkh
M  +1    -0    rkward/pages/rkward_shortcuts.rkh
M  +140  -67   rkward/windows/rkcommandeditorwindow.cpp

https://invent.kde.org/education/rkward/-/commit/3dd8e85f3e4585f475397abd8679a5d52cbc6b15

diff --git a/rkward/misc/rkparsedscript.h b/rkward/misc/rkparsedscript.h
index 2e5635354..4d1262690 100644
--- a/rkward/misc/rkparsedscript.h
+++ b/rkward/misc/rkparsedscript.h
@@ -23,6 +23,12 @@ Inside this flat list, a child context is defined by starting after (or at) the
 contexts are always found after their parent in the list.
 
 Type of context. Parenthesis, Brace, and Bracket, and the outermost context (Top) are the only ContextType s that we actually consider as nested.
+
+-- Addendum/TODO: Wrote that, and implemented it that way, but perhaps it was not the smartes choice after all? The main difficulty is not in
+representing the parsed tree, however, but in walking it in the various different ways we want to support. Some things that might help to make some
+algos here simpler / faster (should we feel the need):
+  - Contexts could store the index of (or a pointer to) their parent -> parentRegion()
+  - Parent contexts could store the index of their *last* child -> easier to determine empty regions, to skip over sub-contexts, and to find end points
 */
 class RKParsedScript {
   public:
diff --git a/rkward/pages/rkward_for_r_users.rkh b/rkward/pages/rkward_for_r_users.rkh
index 6ee76ded9..4f7efb792 100644
--- a/rkward/pages/rkward_for_r_users.rkh
+++ b/rkward/pages/rkward_for_r_users.rkh
@@ -71,6 +71,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 	<li><link href="rkward://page/rkward_console"/></li>
 	<li><link href="rkward://page/rkward_for_new_users"/> For an introduction to some more basic concepts</li>
 	<li><link href="rkward://page/rkward_shortcuts"/></li>
+	<li><link href="rkward://page/rkward_code_navigation"/></li>
 </ul>
 	</related>
 </document>
diff --git a/rkward/pages/rkward_shortcuts.rkh b/rkward/pages/rkward_shortcuts.rkh
index a2fbb06b9..6d298057d 100644
--- a/rkward/pages/rkward_shortcuts.rkh
+++ b/rkward/pages/rkward_shortcuts.rkh
@@ -22,6 +22,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		<li>To cycle between document / tool windows inside RKWard, use Ctrl+Tab and Ctrl+Shift+Tab</li>
 		<li>To run code from scripts / help pages / the console, select it, and use Ctrl+Enter</li>
 		<li>F2 to search the R help for the symbol under the cursor in scripts and the console</li>
+		<li>Meta+n to bring up <link href="rkward://rkward_code_navigation">Code Navigation</link> mode in script windows</li>
 	</ul>
 	</section>
 </document>
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index b7f75f4f2..d612db000 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -22,7 +22,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <QFrame>
 #include <QHBoxLayout>
 #include <QKeyEvent>
-#include <QLabel>
 #include <QLineEdit>
 #include <QMenu>
 #include <QSplitter>
@@ -34,6 +33,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <KIO/DeleteJob>
 #include <KIO/FileCopyJob>
 #include <KLocalizedString>
+#include <KSqueezedTextLabel>
 #include <kactioncollection.h>
 #include <kactionmenu.h>
 #include <kconfiggroup.h>
@@ -67,7 +67,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 
 class RKCodeNavigation : public QWidget {
   private:
-	RKCodeNavigation(KTextEditor::View *view) : QWidget(view, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), view(view), doc(view->document()) {
+	RKCodeNavigation(KTextEditor::View *view, QWidget *parent) : QWidget(parent, Qt::Popup | Qt::FramelessWindowHint | Qt::BypassWindowManagerHint), view(view), doc(view->document()) {
 		auto scheme = RKStyle::viewScheme();
 		auto pal = palette();
 		pal.setColor(backgroundRole(), scheme->background(KColorScheme::PositiveBackground).color());
@@ -77,21 +77,27 @@ class RKCodeNavigation : public QWidget {
 		if (view->window()) view->window()->installEventFilter(this);
 
 		auto box = new QVBoxLayout(this);
-		auto label = new QLabel(i18n("<b>Code Navigation</b> (<a href=\"rkward:://pages/code_navigation\">Help</a>)"));
+		auto label = new QLabel(i18n("<b>Code Navigation</b> (<a href=\"rkward:://pages/rkward_code_navigation\">Help</a>)"));
 		QObject::connect(label, &QLabel::linkActivated, RKWorkplace::mainWorkplace(), &RKWorkplace::openAnyUrlString);
 		box->addWidget(label);
 
-		input = new QLineEdit();
-		input->setPlaceholderText(i18n("e.g. 'ts' - see 'Help', above"));
+		input = new KSqueezedTextLabel();
+		input->setFocusPolicy(Qt::StrongFocus);
+		input->installEventFilter(this);
 		box->addWidget(input);
-		connect(input, &QLineEdit::textChanged, this, &RKCodeNavigation::navigate);
 
-		ps = RKParsedScript(doc->text(), doc->highlightingMode() == u"R Markdown"_s);
+		message = new QLabel();
+		message->hide();
+		box->addWidget(message);
+
+		rmdmode = doc->highlightingMode() == u"R Markdown"_s;
+		ps = RKParsedScript(doc->text(), rmdmode);
 		StoredPosition initial;
 		// translate cursor position to string index
 		initial.pos = cursorToPosition(view->cursorPosition());
 		initial.selection = view->selectionRange();
 		stored_positions.append(initial);
+		stored_size = size();
 	}
 
 	void updatePos() {
@@ -120,76 +126,140 @@ class RKCodeNavigation : public QWidget {
 		return KTextEditor::Cursor();
 	}
 
-	void navigate(const QString &current) {
-		while (!current.startsWith(stored_positions.last().query)) {
-			stored_positions.pop_back();
+	struct StoredPosition {
+		QString query;
+		int pos;
+		KTextEditor::Range selection;
+		bool hit_top;
+		bool hit_bottom;
+		QChar command;
+	};
+
+	void navigate(const StoredPosition &newpos) {
+		RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "navigate to %d", newpos.pos);
+		// translate final position back to cursor coordinates
+		if (!newpos.selection.isEmpty()) {
+			view->setSelection(newpos.selection);
+		} else {
+			if (message->isVisible()) {
+				message->hide();
+				QTimer::singleShot(0, this, [this]() {
+					resize(stored_size);
+					updatePos();
+				});
+			}
+			view->setCursorPosition(positionToCursor(newpos.pos));
 		}
 
+		updateLabel();
+	}
+
+	void handleCommand(const QChar command) {
 		// start from last known position
 		auto last = stored_positions.last();
 		auto pos = last.pos;
-		QString full_command = current.mid(last.query.length());
-
-		// apply navigation command(s)
-		for (int i = 0; i < full_command.size(); ++i) {
-			QChar command = current.back();
-			StoredPosition newpos;
-			newpos.pos = pos;
-			newpos.query = stored_positions.last().query + command;
-
-			auto ci = ps.contextAtPos(pos);
-			if (command == u'n') {
-				newpos.pos = ps.getContext(ps.nextStatement(ci)).start;
-			} else if (command == u'N') {
-				newpos.pos = ps.getContext(ps.prevStatement(ci)).start;
-			} else if (command == u'i') {
-				newpos.pos = ps.getContext(ps.nextStatementOrInner(ci)).start;
-			} else if (command == u'I') {
-				newpos.pos = ps.getContext(ps.prevStatementOrInner(ci)).start;
-			} else if (command == u'o') {
-				newpos.pos = ps.getContext(ps.nextOuter(ci)).start;
-			} else if (command == u'O') {
-				newpos.pos = ps.getContext(ps.prevOuter(ci)).start;
-			} else if (command == u't') {
-				newpos.pos = ps.getContext(ps.nextToplevel(ci)).start;
-			} else if (command == u'T') {
-				newpos.pos = ps.getContext(ps.prevToplevel(ci)).start;
-			} else if (command == u'c') {
-				newpos.pos = ps.getContext(ps.nextCodeChunk(ci)).start;
-			} else if (command == u'C') {
-				newpos.pos = ps.getContext(ps.prevCodeChunk(ci)).start;
-			} else if (command == u's') {
-				auto posa = ps.getContext(ps.firstContextInStatement(ci)).start;
-				auto posb = ps.lastPositionInStatement(ci);
-				newpos.selection = KTextEditor::Range(positionToCursor(posa), positionToCursor(posb + 1));
-			} else {
-				RK_DEBUG(COMMANDEDITOR, DL_WARNING, "unknown navigation commmand");
-				// TODO: show error
-			}
 
-			stored_positions.append(newpos);
-			RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "navigate %d to %d", pos, newpos.pos);
+		// apply navigation command
+		StoredPosition newpos;
+		newpos.pos = pos;
+		newpos.query = stored_positions.last().query + command;
+		newpos.hit_bottom = false;
+		newpos.hit_top = false;
+		newpos.command = command;
+
+		auto ci = ps.contextAtPos(pos);
+		if (command == u'n') {
+			newpos.pos = ps.getContext(ps.nextStatement(ci)).start;
+		} else if (command == u'N') {
+			newpos.pos = ps.getContext(ps.prevStatement(ci)).start;
+		} else if (command == u'i') {
+			newpos.pos = ps.getContext(ps.nextStatementOrInner(ci)).start;
+		} else if (command == u'I') {
+			newpos.pos = ps.getContext(ps.prevStatementOrInner(ci)).start;
+		} else if (command == u'o') {
+			newpos.pos = ps.getContext(ps.nextOuter(ci)).start;
+		} else if (command == u'O') {
+			newpos.pos = ps.getContext(ps.prevOuter(ci)).start;
+		} else if (command == u't') {
+			newpos.pos = ps.getContext(ps.nextToplevel(ci)).start;
+		} else if (command == u'T') {
+			newpos.pos = ps.getContext(ps.prevToplevel(ci)).start;
+		} else if (command == u'c') {
+			newpos.pos = ps.getContext(ps.nextCodeChunk(ci)).start;
+		} else if (command == u'C') {
+			newpos.pos = ps.getContext(ps.prevCodeChunk(ci)).start;
+		} else if (command == u'1') {
+			newpos.pos = 0;
+		} else if (command == u'!') {
+			newpos.pos = cursorToPosition(KTextEditor::Cursor(doc->lines() - 1, doc->lineLength(doc->lines() - 1)));
+		} else if (command == u's') {
+			auto posa = ps.getContext(ps.firstContextInStatement(ci)).start;
+			auto posb = ps.lastPositionInStatement(ci);
+			newpos.selection = KTextEditor::Range(positionToCursor(posa), positionToCursor(posb + 1));
+		} else {
+			RK_DEBUG(COMMANDEDITOR, DL_WARNING, "unknown navigation commmand");
+			message->setText(i18n("Unknown command '%1'").arg(command));
+			message->show();
+			updatePos();
+			return;
 		}
 
-		StoredPosition newpos = stored_positions.last();
-		// translate final position back to cursor coordinates
-		if (!newpos.selection.isEmpty()) {
-			view->setSelection(newpos.selection);
-		} else {
-			view->setCursorPosition(positionToCursor(newpos.pos));
+		if (newpos.pos < 0 && command.toLower() != u's') {
+			if (rmdmode && newpos.command.toLower() != u'c') {
+				if (command.toLower() == command) {
+					message->setText(i18nc("Keep this short", "Search hit bottom.\n'c' to move to next chunk."));
+				} else {
+					message->setText(i18nc("Keep this short", "Search hit top.\n'C' to move to previous chunk."));
+				}
+			} else{
+				if (command.toLower() == command) {
+					message->setText(i18nc("Keep this short", "Search hit bottom.\n'1' to move to top."));
+				} else {
+					message->setText(i18nc("Keep this short", "Search hit top.\n'!' to move to bottom."));
+				}
+			}
+			message->show();
+			updatePos();
+			return;
 		}
+
+		stored_positions.append(newpos);
+		navigate(newpos);
 	}
 
-	bool eventFilter(QObject *, QEvent *event) override {
-		if (event->type() == QEvent::Move || event->type() == QEvent::Resize) {
+	bool eventFilter(QObject *from, QEvent *event) override {
+		if (from == view && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) {
 			updatePos();
+		} else if (from == input && event->type() == QEvent::KeyPress) {
+			auto ke = static_cast<QKeyEvent *>(event);
+			const auto text = ke->text();
+			if (ke->key() == Qt::Key_Backspace) {
+				if (stored_positions.size() > 1) {
+					navigate(stored_positions.takeLast());
+					return true;
+				}
+			} else if (!text.simplified().isEmpty()) {
+				handleCommand(text.back());
+				return true;
+			}
 		}
 		return false;
 	}
 
+	void updateLabel() {
+		if (stored_positions.size() < 2) {
+			input->setText(i18n("Enter command"));
+		} else {
+			QString sequence;
+			for (const auto &p : std::as_const(stored_positions)) {
+				sequence += p.command;
+			}
+			input->setText(sequence);
+		}
+	}
   public:
-	static void doNavigation(KTextEditor::View *view) {
-		auto w = new RKCodeNavigation(view);
+	static void doNavigation(KTextEditor::View *view, QWidget *parent) {
+		auto w = new RKCodeNavigation(view, parent);
 		w->show();
 		w->updatePos();
 		w->input->setFocus();
@@ -198,14 +268,12 @@ class RKCodeNavigation : public QWidget {
   private:
 	KTextEditor::View *view;
 	KTextEditor::Document *doc;
-	QLineEdit *input;
-	struct StoredPosition {
-		QString query;
-		int pos;
-		KTextEditor::Range selection;
-	};
+	KSqueezedTextLabel *input;
 	QList<StoredPosition> stored_positions;
 	RKParsedScript ps;
+	QLabel *message;
+	QSize stored_size;
+	bool rmdmode;
 };
 
 RKCommandEditorWindowPart::RKCommandEditorWindowPart(QWidget *parent) : KParts::Part(parent) {
@@ -548,9 +616,14 @@ void RKCommandEditorWindow::initializeActions(KActionCollection *ac) {
 	file_save_as_action = findAction(m_view, QStringLiteral("file_save_as"));
 	if (file_save_as_action) file_save_as_action->setText(i18n("Save Script As..."));
 
-	action = ac->addAction(QStringLiteral("code_navigation"), this, [this]() { RKCodeNavigation::doNavigation(m_view); });
+	action = ac->addAction(QStringLiteral("code_navigation"), this, [this]() { RKCodeNavigation::doNavigation(m_view, this); });
 	action->setText(i18n("Code Navigation"));
 	ac->setDefaultShortcuts(action, QList<QKeySequence>() << (Qt::MetaModifier | Qt::Key_N));
+
+	const auto actions = standardActionCollection()->actions() + ac->actions();
+	for (QAction *action : actions) {
+		action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
+	}
 }
 
 void RKCommandEditorWindow::initBlocks() {



More information about the rkward-tracker mailing list