[education/rkward] rkward: Add move operators to code navigation

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


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

Add move operators to code navigation

M  +5    -3    rkward/autotests/data/script1.R
M  +11   -0    rkward/autotests/rkparsedscript_test.cpp
M  +22   -15   rkward/misc/rkparsedscript.cpp
M  +1    -1    rkward/misc/rkparsedscript.h
M  +82   -32   rkward/windows/rkcommandeditorwindow.cpp

https://invent.kde.org/education/rkward/-/commit/94492deb1c466d9824ca56e95ad02da97cc66d2f

diff --git a/rkward/autotests/data/script1.R b/rkward/autotests/data/script1.R
index e405c1de5..f548c76af 100644
--- a/rkward/autotests/data/script1.R
+++ b/rkward/autotests/data/script1.R
@@ -1,5 +1,7 @@
-# Comment 1
-# Comment 2
+# - This file is part of the RKWard project (https://rkward.kde.org).
+# SPDX-FileCopyrightText: by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+# SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
+# SPDX-License-Identifier: GPL-2.0-or-later
 Symbol00 + 1
 Symbol01 <- Symbol2(Symbol03="String1", function(Symbol04) {
 	for (Symbol05 in Symbol06) {
@@ -22,7 +24,7 @@ FunctionList <- list(
 	{
 		nest1
 		{
-			nest2(nest2_inner + 1)
+			nest2(n2_inner + 1)
 			nest3()
 			nest4
 		}
diff --git a/rkward/autotests/rkparsedscript_test.cpp b/rkward/autotests/rkparsedscript_test.cpp
index 4a04a82f6..57f4816e0 100644
--- a/rkward/autotests/rkparsedscript_test.cpp
+++ b/rkward/autotests/rkparsedscript_test.cpp
@@ -232,6 +232,17 @@ class RKParsedScriptTest : public QObject {
 		ctx = ps.contextAtPos(script.indexOf(u"jjj"));
 		ctx = moveAndCheck(ps.prevStatementOrInner(ctx), u"ggg"_s);
 		ctx = moveAndCheck(ps.prevStatementOrInner(ctx), u"eee"_s); // TODO: or should it be "fff"?
+		ctx = moveAndCheck(ps.prevStatementOrInner(ctx), u"ddd"_s);
+		ctx = moveAndCheck(ps.prevStatementOrInner(ctx), u"nest5"_s);
+		ctx = moveAndCheck(ps.prevStatementOrInner(ctx), u"nest4"_s);
+	}
+
+	void range() {
+		loadScript(u"script1.R"_s);
+		auto ctx = ps.contextAtPos(script.indexOf(u"Symbol01"));
+		QCOMPARE(script.mid(ps.lastPositionInStatement(ctx)+1, 9), u"\nSymbol19"_s);
+		ctx = ps.contextAtPos(script.indexOf(u"Symbol08"));
+		QCOMPARE(script.at(ps.lastPositionInStatement(ctx)), u']');
 	}
 };
 
diff --git a/rkward/misc/rkparsedscript.cpp b/rkward/misc/rkparsedscript.cpp
index a3c2c12ec..9aa19be9b 100644
--- a/rkward/misc/rkparsedscript.cpp
+++ b/rkward/misc/rkparsedscript.cpp
@@ -56,7 +56,7 @@ int RKParsedScript::addContext(ContextType type, int start, const QString &conte
 	} else if (type == AnySymbol) {
 		while (++pos < content.length()) {
 			const QChar c = content.at(pos);
-			if (!c.isLetterOrNumber() && c != u'.') {
+			if (!c.isLetterOrNumber() && c != u'.' && c != u'_') {
 				--pos;
 				break;
 			}
@@ -222,6 +222,17 @@ RKParsedScript::ContextIndex RKParsedScript::lastContextInStatement(const Contex
 	return prevContext(ni);
 }
 
+int RKParsedScript::lastPositionInStatement(const ContextIndex from) const {
+	RK_TRACE(MISC);
+	auto last = lastContextInStatement(from);
+	auto first = firstContextInStatement(from);
+	// consider "a <- b({ c })". We want the position at ")", not at "c"
+	while (parentRegion(last) != parentRegion(first) && last.valid()) {
+		last = parentRegion(last);
+	}
+	return getContext(last).end;
+}
+
 RKParsedScript::ContextIndex RKParsedScript::nextStatement(const ContextIndex from) const {
 	RK_TRACE(MISC);
 
@@ -307,22 +318,18 @@ RKParsedScript::ContextIndex RKParsedScript::nextStatementOrInner(const ContextI
 RKParsedScript::ContextIndex RKParsedScript::prevStatementOrInner(const ContextIndex from) const {
 	RK_TRACE(MISC);
 
-	auto psi = prevStatement(from);
+	auto psi = prevStatement(from); // this is the target, *unless* there is an inner context on the way
+	auto parent = parentRegion(from);
 	auto ci = prevContext(from);
 	while (ci.valid() && ci.index > psi.index) {
-		auto ctx = getContext(ci);
-		if (ctx.maybeNesting()) {
-			auto candidate = nextContext(ci);
-			if (parentRegion(candidate) == ci && candidate != from) {
-				// found an inner region, but what's the last (inner!) statement in it?
-				// TODO: is there an easier solution?
-				auto next = candidate;
-				auto limit = nextOuter(candidate);
-				while (next.index < limit.index && next.index < ci.index) {
-					candidate = next;
-					next = nextStatementOrInner(candidate);
-				} 
-				return candidate;
+		auto cparent = parentRegion(ci);
+		if (cparent != parent) {
+			// We entered a different parent region. What's the last (inner!) context in it?
+			auto candidate = nextContext(cparent);
+			while (true) {
+				ci = nextStatementOrInner(candidate);
+				if (ci.index >= from.index) return candidate;
+				candidate = ci;
 			}
 		}
 		ci = prevContext(ci);
diff --git a/rkward/misc/rkparsedscript.h b/rkward/misc/rkparsedscript.h
index 8a5f61db7..efb80a94c 100644
--- a/rkward/misc/rkparsedscript.h
+++ b/rkward/misc/rkparsedscript.h
@@ -90,6 +90,7 @@ class RKParsedScript {
 
 	ContextIndex firstContextInStatement(const ContextIndex from) const;
 	ContextIndex lastContextInStatement(const ContextIndex from) const;
+	int lastPositionInStatement(const ContextIndex from) const;
 
 	ContextIndex nextOuter(const ContextIndex from) const;
 	ContextIndex prevOuter(const ContextIndex from) const;
@@ -112,7 +113,6 @@ class RKParsedScript {
 	int addContext(ContextType type, int start, const QString &content);
 
 	friend class RKParsedScriptTest;
-	friend class RKCodeNavigation;
 	// NOTE: used in debugging, only
 	QString serialize() const;
 	QString serializeContextEnd(const Context &ctx, int level) const;
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index 531f9d5fa..3b93b1055 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -42,7 +42,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <kstandardaction.h>
 #include <ktexteditor_version.h>
 #include <kwidgetsaddons_version.h>
-#include <qcombobox.h>
 
 #include "../core/robjectlist.h"
 #include "../misc/rkcommonfunctions.h"
@@ -82,14 +81,19 @@ class RKCodeNavigation : public QWidget {
 		QObject::connect(label, &QLabel::linkActivated, RKWorkplace::mainWorkplace(), &RKWorkplace::openAnyUrlString);
 		box->addWidget(label);
 
-		input = new QComboBox();
-		input->setEditable(true);
-		input->addItem(QStringLiteral("s    # ") + i18n("Expand selection to statement"));
-		input->addItem(QStringLiteral("t    # ") + i18n("Go to next top level statement"));
-		input->lineEdit()->setPlaceholderText(i18n("e.g. 'ts' - see 'Help', above"));
-		input->setCurrentIndex(-1);
+		input = new QLineEdit();
+		input->setPlaceholderText(i18n("e.g. 'ts' - see 'Help', above"));
 		box->addWidget(input);
-		connect(input, &QComboBox::currentTextChanged, this, &RKCodeNavigation::navigate);
+		connect(input, &QLineEdit::textChanged, this, &RKCodeNavigation::navigate);
+
+		ps = RKParsedScript(doc->text());
+		StoredPosition initial;
+		// translate cursor position to string index
+		initial.pos = cursorToPosition(view->cursorPosition());
+		initial.selection = view->selectionRange();
+		stored_positions.append(initial);
+
+		bool multilanguage = doc->embeddedHighlightingModes().size() > 1;
 	}
 
 	void updatePos() {
@@ -100,38 +104,77 @@ class RKCodeNavigation : public QWidget {
 		deleteLater();
 	}
 
-	void navigate(const QString &current) {
-		// TODO: cache the parse tree. But for testing, it's not so bad to have it all parsed per keypress
-		RKParsedScript tree(doc->text());
-		qDebug("%s", qPrintable(tree.serialize()));
-
-		// translate cursor position to string index
-		const auto cursor = view->cursorPosition();
+	int cursorToPosition(const KTextEditor::Cursor &cursor) {
 		int pos = cursor.column();
 		for (int l = 0; l < cursor.line(); ++l) {
 			pos += doc->lineLength(l) + 1; 
 		}
+		return pos;
+	}
 
-		// then find out, where that is in the parse tree
-		auto ci = tree.contextAtPos(pos);
+	KTextEditor::Cursor positionToCursor(int pos) {
+		for (int l = 0; l < doc->lines(); ++l) {
+			pos -= (doc->lineLength(l) + 1);
+			if (pos < 0) {
+				return KTextEditor::Cursor(l, pos + doc->lineLength(l) + 1);
+			}
+		}
+		return KTextEditor::Cursor();
+	}
 
-		// apply navigation command
-		QChar command = current.back();
-		int newpos = pos;
-		if (command == u'n') {
-			newpos = tree.getContext(tree.nextStatement(ci)).start;
-		} else if (command == u'N') {
-			newpos = tree.getContext(tree.prevStatement(ci)).start;
+	void navigate(const QString &current) {
+		while (!current.startsWith(stored_positions.last().query)) {
+			stored_positions.pop_back();
 		}
-		RK_DEBUG(COMMANDEDITOR, DL_DEBUG, "navigate %d to %d", pos, newpos);
 
-		// translate new position back to cursor coordinates
-		for (int l = 0; l < doc->lines(); ++l) {
-			newpos -= (doc->lineLength(l) + 1);
-			if (newpos < 0) {
-				view->setCursorPosition(KTextEditor::Cursor(l, newpos + doc->lineLength(l) + 1));
-				break;
+		// 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'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);
+		}
+
+		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));
 		}
 	}
 
@@ -151,7 +194,14 @@ class RKCodeNavigation : public QWidget {
   private:
 	KTextEditor::View *view;
 	KTextEditor::Document *doc;
-	QComboBox *input;
+	QLineEdit *input;
+	struct StoredPosition {
+		QString query;
+		int pos;
+		KTextEditor::Range selection;
+	};
+	QList<StoredPosition> stored_positions;
+	RKParsedScript ps;
 };
 
 RKCommandEditorWindowPart::RKCommandEditorWindowPart(QWidget *parent) : KParts::Part(parent) {



More information about the rkward-tracker mailing list