[education/rkward] rkward/windows: Some initial bits for code navigation
Thomas Friedrichsmeier
null at kde.org
Mon May 26 15:06:38 BST 2025
Git commit e2dba2456b6383856747fb79b49258e071e6c527 by Thomas Friedrichsmeier.
Committed on 26/05/2025 at 14:06.
Pushed by tfry into branch 'master'.
Some initial bits for code navigation
M +216 -0 rkward/windows/rkcommandeditorwindow.cpp
https://invent.kde.org/education/rkward/-/commit/e2dba2456b6383856747fb79b49258e071e6c527
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index df2004fb1..14a94b0bd 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -23,6 +23,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
+#include <QLineEdit>
#include <QMenu>
#include <QSplitter>
#include <QTemporaryDir>
@@ -34,18 +35,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include <KLocalizedString>
#include <kactioncollection.h>
#include <kactionmenu.h>
+#include <KColorScheme>
#include <kconfiggroup.h>
#include <kmessagebox.h>
#include <krandom.h>
#include <kstandardaction.h>
#include <ktexteditor_version.h>
#include <kwidgetsaddons_version.h>
+#include <qcombobox.h>
#include "../core/robjectlist.h"
#include "../misc/rkcommonfunctions.h"
#include "../misc/rkjobsequence.h"
#include "../misc/rkstandardactions.h"
#include "../misc/rkstandardicons.h"
+#include "../misc/rkstyle.h"
#include "../misc/rkxmlguipreviewarea.h"
#include "../misc/rkxmlguisyncer.h"
#include "../rbackend/rkrinterface.h"
@@ -63,6 +67,213 @@ SPDX-License-Identifier: GPL-2.0-or-later
#include "../debug.h"
+/** Very crude, but very fast R parser. Parses the basic structure, only */
+class RKScriptContext {
+public:
+ enum ContextType {
+ Top,
+ Parenthesis,
+ Brace,
+ Bracket,
+ Comment,
+ SingleQuoted,
+ DoubleQuoted,
+ BackQuoted,
+ SubsetOperator,
+ OtherOperator,
+ Delimiter,
+ AnySymbol
+ } type;
+
+ RKScriptContext(ContextType type, int start, const QStringView &content) : type(type), start(start) {
+ int pos = start-1;
+
+ if (type == SingleQuoted || type == DoubleQuoted || type == BackQuoted) {
+ while (++pos < content.length()) {
+ const QChar c = content.at(pos);
+ if (c == u'\\') ++pos;
+ else if (c == u'\'' && type == SingleQuoted) break;
+ else if (c == u'"' && type == DoubleQuoted) break;
+ else if (c == u'`' && type == BackQuoted) break;
+ }
+ } else if (type == AnySymbol) {
+ while (++pos < content.length()) {
+ const QChar c = content.at(pos);
+ if (!c.isLetterOrNumber() && c != u'.') {
+ --pos;
+ break;
+ }
+ }
+ } else if (type == Comment) {
+ while (++pos < content.length()) {
+ if (content.at(pos) == u'\n') break;
+ }
+ } else if (type == OtherOperator || type == SubsetOperator || type == Delimiter) {
+ // leave context, immediately
+ } else {
+ while (++pos < content.length()) {
+ QChar c = content.at(pos);
+ if (c == u'\'') pos = addContext(SingleQuoted, pos, content);
+ else if (c == u'"') pos = addContext(DoubleQuoted, pos, content);
+ else if (c == u'`') pos = addContext(BackQuoted, pos, content);
+ else if (c == u'#') pos = addContext(Comment, pos, content);
+ else if (c == u'(') pos = addContext(Parenthesis, pos, content);
+ else if (c == u')' && type == Parenthesis) break;
+ else if (c == u'{') pos = addContext(Brace, pos, content);
+ else if (c == u'}' && type == Brace) break;
+ else if (c == u'[') pos = addContext(Bracket, pos, content);
+ else if (c == u']' && type == Bracket) break;
+ else if (c.isLetterOrNumber() || c == u'.') pos = addContext(AnySymbol, pos, content);
+ else if (c == u'\n' || c == u',' || c == u';') pos = addContext(Delimiter, pos, content);
+ else if (c == u'$' || c == u'@') pos = addContext(SubsetOperator, pos, content);
+ else if (!c.isSpace()) pos = addContext(OtherOperator, pos, content);
+ }
+ }
+
+ end = pos;
+ }
+
+ int addContext(ContextType type, int start, const QStringView &content) {
+ auto ctx = RKScriptContext(type, start+1, content);
+ // post cleanups (all these depend on the previous context at this level:
+ if (!children.isEmpty()) {
+ auto &prev = children.last();
+ auto prevtype = prev.type;
+ // Merge any two subsequent operators into one token
+ if (type == OtherOperator && prevtype == OtherOperator) {
+ prev.end = ctx.end;
+ return ctx.end;
+ }
+ // newlines do not count as delimiter on operator RHS
+ if (type == Delimiter && content.at(start) == '\n' && (prevtype == OtherOperator || prevtype == SubsetOperator)) {
+ return ctx.end;
+ }
+ // TODO: post cleanup (possibly incomplete list):
+ // - special treatment for subsetting operators: Treat as continuation of symbol -> but maybe not here?
+ }
+
+ children.append(ctx);
+ return ctx.end;
+ }
+
+ // purely for debugging:
+ QString serialize(int level=0) const {
+ QString ret;
+
+ if (type == Parenthesis) ret += u'(';
+ if (type == Brace) ret += u'{';
+ if (type == Bracket) ret += u'[';
+ if (type == SingleQuoted) ret += u'\'';
+ if (type == DoubleQuoted) ret += u'"';
+ if (type == BackQuoted) ret += u'`';
+ if (type == Comment) ret += u'#';
+ if (type == SubsetOperator) ret += u'$';
+ if (type == OtherOperator) ret += u'+';
+ if (type == AnySymbol) ret += u'x';
+
+ for(const auto &c : std::as_const(children)) ret += c.serialize(level+4);
+
+ if (type == Parenthesis) ret += u')';
+ if (type == Brace) ret += u'}';
+ if (type == Bracket) ret += u']';
+ if (type == SingleQuoted) ret += u'\'';
+ if (type == DoubleQuoted) ret += u'"';
+ if (type == BackQuoted) ret += u'`';
+ if (type == Comment || type == Delimiter) {
+ ret += u'\n';
+ for (int i = 0; i < level; ++i) ret.append(u' ');
+ }
+ return ret;
+ }
+
+ int start;
+ int end;
+ QList<RKScriptContext> children;
+};
+
+class RKCodeNavigation : public QWidget {
+ private:
+ RKCodeNavigation(KTextEditor::View *view) : QWidget(view, 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());
+ setPalette(pal);
+
+ view->installEventFilter(this);
+ 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>)"));
+ 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);
+ box->addWidget(input);
+ connect(input, &QComboBox::currentTextChanged, this, &RKCodeNavigation::navigate);
+ }
+
+ void updatePos() {
+ move(view->mapToGlobal(view->geometry().topRight() - QPoint(width(), 0)));
+ }
+
+ void focusOutEvent(QFocusEvent *) override {
+ deleteLater();
+ }
+
+ void navigate(const QString ¤t) {
+ qDebug("%s", qPrintable(RKScriptContext(RKScriptContext::Top, 0, doc->text()).serialize()));
+/* auto cursor = view->cursorPosition();
+ enum { Seek, FoundStart, FoundEnd, eof } status = Seek;
+ for (; cursor.advance(); status != Seek) {
+ auto s = doc->defaultStyleAt(cursor);
+ // Skip over all comments (any easier to type logic for this?)
+ if (s == KSyntaxHighlighting::Theme::Comment ||
+ s == KSyntaxHighlighting::Theme::Documentation ||
+ s == KSyntaxHighlighting::Theme::Annotation ||
+ s == KSyntaxHighlighting::Theme::CommentVar ||
+ s == KSyntaxHighlighting::Theme::RegionMarker ||
+ s == KSyntaxHighlighting::Theme::Information ||
+ s == KSyntaxHighlighting::Theme::Warning ||
+ s == KSyntaxHighlighting::Theme::Alert) {
+ continue;
+ }
+ // Skip over all strings (any easier to type logic for this?)
+ if (s == KSyntaxHighlighting::Theme::Char ||
+ s == KSyntaxHighlighting::Theme::SpecialChar ||
+ s == KSyntaxHighlighting::Theme::String ||
+ s == KSyntaxHighlighting::Theme::VerbatimString ||
+ s == KSyntaxHighlighting::Theme::SpecialString) {
+ continue;
+ }
+ if (s == KSyntaxHighlighting::Theme::Operator ) {
+ }
+ } */
+ }
+
+ bool eventFilter(QObject *, QEvent *event) override {
+ if (event->type() == QEvent::Move || event->type() == QEvent::Resize) {
+ updatePos();
+ }
+ return false;
+ }
+ public:
+ static void doNavigation(KTextEditor::View *view) {
+ auto w = new RKCodeNavigation(view);
+ w->show();
+ w->updatePos();
+ w->input->setFocus();
+ }
+ private:
+ KTextEditor::View *view;
+ KTextEditor::Document *doc;
+ QComboBox *input;
+};
+
RKCommandEditorWindowPart::RKCommandEditorWindowPart(QWidget *parent) : KParts::Part(parent) {
RK_TRACE(COMMANDEDITOR);
@@ -402,6 +613,10 @@ void RKCommandEditorWindow::initializeActions(KActionCollection *ac) {
if (file_save_action) file_save_action->setText(i18n("Save Script..."));
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->setText(i18n("Code Navigation"));
+ ac->setDefaultShortcuts(action, QList<QKeySequence>() << (Qt::MetaModifier | Qt::Key_N));
}
void RKCommandEditorWindow::initBlocks() {
@@ -970,6 +1185,7 @@ void RKCommandEditorWindow::setWDToScript() {
dir.remove(0, 1);
#endif
RKConsole::pipeUserCommand(u"setwd (\""_s + dir + u"\")"_s);
+ m_view->insertTemplate(m_view->cursorPosition(), u"${dummy()}"_s, u"require('document.js'); require('view.js'); document.insertText(view.cursorPosition(), 'INSERT'); view.setSelection(0,2, 0,4); function dummy() {};"_s);
}
void RKCommandEditorWindow::runCurrent() {
More information about the rkward-tracker
mailing list