[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 ¤t) {
- // 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 ¤t) {
+ 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