M5: :-verb command pack (sort/case/base64/rot13/uuid/pipe)
Add src/commands/: texttransforms (pure, unit-tested string transforms) and olliecommands (a KTextEditor::Command registering sort, rsort, case, b64enc, b64dec, rot13, uuid, pipe). Verbs act on the selection in place, or the whole document when nothing is selected. 'pipe <shell>' runs sh -c <shell> with the selection on stdin and replaces it with stdout (the user types the command, so shell execution is intended). The command object is created once by OlliePlugin and auto-registers with the global editor. 15 transform unit tests; 6 ctest suites green overall. Verbs are reachable from Kate's command line; surfacing them in the M-x palette is a follow-up (they are Commands, not QActions).
This commit is contained in:
parent
bfbb32a95a
commit
217d042a1b
|
|
@ -20,6 +20,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|||
|
||||
find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Gui Widgets)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Test)
|
||||
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS TextEditor XmlGui CoreAddons I18n)
|
||||
|
||||
enable_testing()
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ src/
|
|||
radialconfig.{h,cpp} JSON → radial tree + built-in default
|
||||
test_radialmodel.cpp QTest suite
|
||||
test_radialconfig.cpp QTest suite
|
||||
commands/
|
||||
texttransforms.{h,cpp} pure text transforms (headless)
|
||||
olliecommands.{h,cpp} ":"-verb KTextEditor::Command pack
|
||||
test_texttransforms.cpp QTest suite
|
||||
plugin/
|
||||
ollieplugin.{h,cpp} KTextEditor::Plugin + M-x + radial wiring
|
||||
ollieplugin.json plugin metadata
|
||||
|
|
|
|||
20
docs/PLAN.md
20
docs/PLAN.md
|
|
@ -170,13 +170,19 @@ Therefore the palette is replaced, not extended, and the replacement is built on
|
|||
these combos (so Kate's shortcut never fires) and acts on the `KeyPress`,
|
||||
consuming it — no registered shortcut, so no ambiguity, no per-view sweeping.
|
||||
|
||||
### M5 — Command vocabulary + project model + switchers — NEXT
|
||||
- `:`-verb pack via `KTextEditor::Command`: `sort`, `align`, `json`, `b64`,
|
||||
`uuid`, `case`, `pipe <shell>`, etc. These populate M-x and the radial.
|
||||
- **Project = directory.** "Open folder == open project" (VSCode/Sublime model);
|
||||
no `.kateproject` ceremony.
|
||||
- File / symbol switchers scoped to the opened folder (e.g. `git ls-files` / `fd`
|
||||
for files; LSP or ctags for symbols), all through the M2 palette.
|
||||
### M5 — Command vocabulary + project model + switchers — IN PROGRESS
|
||||
- `:`-verb pack — DONE. `src/commands/`: `texttransforms.{h,cpp}` (pure,
|
||||
unit-tested: sort/rsort, case upper|lower|title|snake|camel|kebab, base64
|
||||
enc/dec, rot13, uuid) + `olliecommands.{h,cpp}` (a `KTextEditor::Command`
|
||||
registering `sort`, `rsort`, `case`, `b64enc`, `b64dec`, `rot13`, `uuid`,
|
||||
`pipe`). Verbs transform the selection in place (or the whole document when
|
||||
nothing is selected); `pipe <shell>` runs `sh -c <shell>` with the selection
|
||||
on stdin and replaces it with stdout. 15 transform tests. The command object
|
||||
is created once by `OlliePlugin` (auto-registers globally).
|
||||
- Reachable from Kate's command line today; surfacing the verbs in the M-x
|
||||
palette (they are `Command`s, not `QAction`s) is a follow-up.
|
||||
- Project = directory ("open folder == open project"): TODO.
|
||||
- Fuzzy file / symbol switchers scoped to the folder: TODO.
|
||||
|
||||
## Build and test
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
add_subdirectory(fuzzy)
|
||||
add_subdirectory(palette)
|
||||
add_subdirectory(radial)
|
||||
if(KF6TextEditor_FOUND)
|
||||
add_subdirectory(commands)
|
||||
endif()
|
||||
add_subdirectory(plugin)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
add_library(olliecommands_lib STATIC
|
||||
texttransforms.cpp
|
||||
texttransforms.h
|
||||
olliecommands.cpp
|
||||
olliecommands.h
|
||||
)
|
||||
target_link_libraries(olliecommands_lib PUBLIC
|
||||
Qt6::Core
|
||||
KF6::TextEditor
|
||||
KF6::I18n
|
||||
)
|
||||
target_include_directories(olliecommands_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
if(Qt6Test_FOUND)
|
||||
add_executable(test_texttransforms test_texttransforms.cpp texttransforms.cpp texttransforms.h)
|
||||
target_link_libraries(test_texttransforms PRIVATE Qt6::Core Qt6::Test)
|
||||
add_test(NAME texttransforms COMMAND test_texttransforms)
|
||||
endif()
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "olliecommands.h"
|
||||
|
||||
#include "texttransforms.h"
|
||||
|
||||
#include <KTextEditor/Document>
|
||||
#include <KTextEditor/View>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QProcess>
|
||||
#include <QStringList>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
using namespace texttransforms;
|
||||
|
||||
QStringList OllieCommands::supportedCmds()
|
||||
{
|
||||
return {
|
||||
QStringLiteral("sort"),
|
||||
QStringLiteral("rsort"),
|
||||
QStringLiteral("case"),
|
||||
QStringLiteral("b64enc"),
|
||||
QStringLiteral("b64dec"),
|
||||
QStringLiteral("rot13"),
|
||||
QStringLiteral("uuid"),
|
||||
QStringLiteral("pipe"),
|
||||
};
|
||||
}
|
||||
|
||||
OllieCommands::OllieCommands(QObject *parent)
|
||||
: KTextEditor::Command(supportedCmds(), parent)
|
||||
{
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// The text the verb should operate on, and the range to replace. With a
|
||||
// selection, that is the selection; otherwise the whole document.
|
||||
struct Target {
|
||||
QString text;
|
||||
KTextEditor::Range range;
|
||||
};
|
||||
|
||||
Target currentTarget(KTextEditor::View *view)
|
||||
{
|
||||
if (view->selection()) {
|
||||
return {view->selectionText(), view->selectionRange()};
|
||||
}
|
||||
KTextEditor::Document *doc = view->document();
|
||||
return {doc->text(), doc->documentRange()};
|
||||
}
|
||||
|
||||
void applyResult(KTextEditor::View *view, const Target &t, const QString &result)
|
||||
{
|
||||
view->document()->replaceText(t.range, result);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool OllieCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg,
|
||||
const KTextEditor::Range &)
|
||||
{
|
||||
if (!view) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QStringList parts = cmd.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QString verb = parts.at(0);
|
||||
const QStringList args = parts.mid(1);
|
||||
|
||||
// uuid inserts at the cursor; it does not need a target selection.
|
||||
if (verb == QLatin1String("uuid")) {
|
||||
view->document()->insertText(view->cursorPosition(), texttransforms::uuid());
|
||||
return true;
|
||||
}
|
||||
|
||||
const Target t = currentTarget(view);
|
||||
|
||||
if (verb == QLatin1String("sort")) {
|
||||
const bool unique = args.contains(QLatin1String("u"));
|
||||
const bool ci = args.contains(QLatin1String("i"));
|
||||
applyResult(view, t, sortLines(t.text, unique, ci));
|
||||
return true;
|
||||
}
|
||||
if (verb == QLatin1String("rsort")) {
|
||||
applyResult(view, t, reverseLines(t.text));
|
||||
return true;
|
||||
}
|
||||
if (verb == QLatin1String("case")) {
|
||||
if (args.isEmpty()) {
|
||||
msg = i18n("case: need a style (upper|lower|title|snake|camel|kebab)");
|
||||
return false;
|
||||
}
|
||||
const QString s = args.at(0).toLower();
|
||||
CaseStyle style;
|
||||
if (s == QLatin1String("upper")) {
|
||||
style = CaseStyle::Upper;
|
||||
} else if (s == QLatin1String("lower")) {
|
||||
style = CaseStyle::Lower;
|
||||
} else if (s == QLatin1String("title")) {
|
||||
style = CaseStyle::Title;
|
||||
} else if (s == QLatin1String("snake")) {
|
||||
style = CaseStyle::Snake;
|
||||
} else if (s == QLatin1String("camel")) {
|
||||
style = CaseStyle::Camel;
|
||||
} else if (s == QLatin1String("kebab")) {
|
||||
style = CaseStyle::Kebab;
|
||||
} else {
|
||||
msg = i18n("case: unknown style '%1'", s);
|
||||
return false;
|
||||
}
|
||||
applyResult(view, t, convertCase(t.text, style));
|
||||
return true;
|
||||
}
|
||||
if (verb == QLatin1String("b64enc")) {
|
||||
applyResult(view, t, base64Encode(t.text));
|
||||
return true;
|
||||
}
|
||||
if (verb == QLatin1String("b64dec")) {
|
||||
applyResult(view, t, base64Decode(t.text));
|
||||
return true;
|
||||
}
|
||||
if (verb == QLatin1String("rot13")) {
|
||||
applyResult(view, t, rot13(t.text));
|
||||
return true;
|
||||
}
|
||||
if (verb == QLatin1String("pipe")) {
|
||||
if (args.isEmpty()) {
|
||||
msg = i18n("pipe: need a shell command");
|
||||
return false;
|
||||
}
|
||||
// The user typed the shell command explicitly, so running it through
|
||||
// the shell is the intended behaviour. Feed the target text on stdin
|
||||
// and replace it with the command's stdout.
|
||||
const QString shellCmd = cmd.section(QLatin1Char(' '), 1); // keep spaces/quotes
|
||||
QProcess proc;
|
||||
proc.start(QStringLiteral("sh"), {QStringLiteral("-c"), shellCmd});
|
||||
if (!proc.waitForStarted(3000)) {
|
||||
msg = i18n("pipe: failed to start shell");
|
||||
return false;
|
||||
}
|
||||
proc.write(t.text.toUtf8());
|
||||
proc.closeWriteChannel();
|
||||
if (!proc.waitForFinished(15000)) {
|
||||
proc.kill();
|
||||
msg = i18n("pipe: command timed out");
|
||||
return false;
|
||||
}
|
||||
const QByteArray out = proc.readAllStandardOutput();
|
||||
applyResult(view, t, QString::fromUtf8(out));
|
||||
if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) {
|
||||
msg = i18n("pipe: command exited with status %1", proc.exitCode());
|
||||
// Output already applied; report but do not fail the edit.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OllieCommands::help(KTextEditor::View *, const QString &cmd, QString &msg)
|
||||
{
|
||||
const QString verb = cmd.section(QLatin1Char(' '), 0, 0);
|
||||
if (verb == QLatin1String("sort")) {
|
||||
msg = i18n("sort [u] [i] — sort selected lines; u=unique, i=case-insensitive");
|
||||
} else if (verb == QLatin1String("rsort")) {
|
||||
msg = i18n("rsort — reverse the order of selected lines");
|
||||
} else if (verb == QLatin1String("case")) {
|
||||
msg = i18n("case <upper|lower|title|snake|camel|kebab> — convert selection case");
|
||||
} else if (verb == QLatin1String("b64enc") || verb == QLatin1String("b64dec")) {
|
||||
msg = i18n("b64enc / b64dec — base64 encode or decode the selection");
|
||||
} else if (verb == QLatin1String("rot13")) {
|
||||
msg = i18n("rot13 — ROT13 the selection");
|
||||
} else if (verb == QLatin1String("uuid")) {
|
||||
msg = i18n("uuid — insert a random UUID at the cursor");
|
||||
} else if (verb == QLatin1String("pipe")) {
|
||||
msg = i18n("pipe <shell> — replace selection with output of sh -c <shell> (selection on stdin)");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace katecustom
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* OllieCommands registers a small vocabulary of ":" command-line verbs with
|
||||
* the editor. Each verb transforms the current selection in place (or the whole
|
||||
* document when nothing is selected). Verbs:
|
||||
*
|
||||
* sort [u] [i] sort selected lines (u = unique, i = case-insensitive)
|
||||
* rsort reverse the order of selected lines
|
||||
* case <style> upper|lower|title|snake|camel|kebab
|
||||
* b64enc / b64dec base64 encode / decode
|
||||
* rot13 ROT13
|
||||
* uuid insert a random UUID at the cursor
|
||||
* pipe <shell> replace the selection with the output of `sh -c <shell>`,
|
||||
* feeding the selection on stdin
|
||||
*/
|
||||
#ifndef KATECUSTOM_OLLIECOMMANDS_H
|
||||
#define KATECUSTOM_OLLIECOMMANDS_H
|
||||
|
||||
#include <KTextEditor/Command>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
class OllieCommands : public KTextEditor::Command
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OllieCommands(QObject *parent = nullptr);
|
||||
|
||||
bool exec(KTextEditor::View *view, const QString &cmd, QString &msg,
|
||||
const KTextEditor::Range &range = KTextEditor::Range::invalid()) override;
|
||||
bool help(KTextEditor::View *view, const QString &cmd, QString &msg) override;
|
||||
|
||||
private:
|
||||
static QStringList supportedCmds();
|
||||
};
|
||||
|
||||
} // namespace katecustom
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "texttransforms.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QRegularExpression>
|
||||
|
||||
using namespace katecustom;
|
||||
using namespace katecustom::texttransforms;
|
||||
|
||||
class TestTextTransforms : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
|
||||
void sortBasic()
|
||||
{
|
||||
QCOMPARE(sortLines(QStringLiteral("banana\napple\ncherry")),
|
||||
QStringLiteral("apple\nbanana\ncherry"));
|
||||
}
|
||||
|
||||
void sortPreservesTrailingNewline()
|
||||
{
|
||||
QCOMPARE(sortLines(QStringLiteral("b\na\n")), QStringLiteral("a\nb\n"));
|
||||
}
|
||||
|
||||
void sortUnique()
|
||||
{
|
||||
QCOMPARE(sortLines(QStringLiteral("b\na\nb\na"), /*unique=*/true),
|
||||
QStringLiteral("a\nb"));
|
||||
}
|
||||
|
||||
void sortCaseInsensitive()
|
||||
{
|
||||
QCOMPARE(sortLines(QStringLiteral("B\na\nC"), false, /*ci=*/true),
|
||||
QStringLiteral("a\nB\nC"));
|
||||
}
|
||||
|
||||
void reverse()
|
||||
{
|
||||
QCOMPARE(reverseLines(QStringLiteral("one\ntwo\nthree")),
|
||||
QStringLiteral("three\ntwo\none"));
|
||||
}
|
||||
|
||||
void caseUpperLower()
|
||||
{
|
||||
QCOMPARE(convertCase(QStringLiteral("Hello"), CaseStyle::Upper), QStringLiteral("HELLO"));
|
||||
QCOMPARE(convertCase(QStringLiteral("Hello"), CaseStyle::Lower), QStringLiteral("hello"));
|
||||
}
|
||||
|
||||
void caseTitle()
|
||||
{
|
||||
QCOMPARE(convertCase(QStringLiteral("hello wORLD foo"), CaseStyle::Title),
|
||||
QStringLiteral("Hello World Foo"));
|
||||
}
|
||||
|
||||
void caseSnakeFromCamel()
|
||||
{
|
||||
QCOMPARE(convertCase(QStringLiteral("helloWorldFoo"), CaseStyle::Snake),
|
||||
QStringLiteral("hello_world_foo"));
|
||||
}
|
||||
|
||||
void caseKebabFromSpaces()
|
||||
{
|
||||
QCOMPARE(convertCase(QStringLiteral("Hello World Foo"), CaseStyle::Kebab),
|
||||
QStringLiteral("hello-world-foo"));
|
||||
}
|
||||
|
||||
void caseCamelFromSnake()
|
||||
{
|
||||
QCOMPARE(convertCase(QStringLiteral("hello_world_foo"), CaseStyle::Camel),
|
||||
QStringLiteral("helloWorldFoo"));
|
||||
}
|
||||
|
||||
void base64RoundTrip()
|
||||
{
|
||||
const QString s = QStringLiteral("Hello, 世界!");
|
||||
QCOMPARE(base64Decode(base64Encode(s)), s);
|
||||
}
|
||||
|
||||
void base64KnownVector()
|
||||
{
|
||||
QCOMPARE(base64Encode(QStringLiteral("hello")), QStringLiteral("aGVsbG8="));
|
||||
QCOMPARE(base64Decode(QStringLiteral("aGVsbG8=")), QStringLiteral("hello"));
|
||||
}
|
||||
|
||||
void rot13Involution()
|
||||
{
|
||||
const QString s = QStringLiteral("Why did the chicken? 123");
|
||||
QCOMPARE(rot13(rot13(s)), s);
|
||||
QCOMPARE(rot13(QStringLiteral("abc XYZ")), QStringLiteral("nop KLM"));
|
||||
}
|
||||
|
||||
void uuidShape()
|
||||
{
|
||||
const QString u = uuid();
|
||||
QRegularExpression re(QStringLiteral(
|
||||
"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"));
|
||||
QVERIFY2(re.match(u).hasMatch(), qPrintable(u));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(TestTextTransforms)
|
||||
#include "test_texttransforms.moc"
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "texttransforms.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
namespace texttransforms
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
// Split into lines while remembering whether the text ended with a newline, so
|
||||
// the transform can restore it. Uses '\n' as the separator (Kate normalises
|
||||
// line endings to '\n' in document text).
|
||||
QStringList splitLines(const QString &text, bool &trailingNewline)
|
||||
{
|
||||
trailingNewline = text.endsWith(QLatin1Char('\n'));
|
||||
QString body = text;
|
||||
if (trailingNewline) {
|
||||
body.chop(1);
|
||||
}
|
||||
return body.split(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
QString joinLines(const QStringList &lines, bool trailingNewline)
|
||||
{
|
||||
QString out = lines.join(QLatin1Char('\n'));
|
||||
if (trailingNewline) {
|
||||
out.append(QLatin1Char('\n'));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Split an identifier-ish string into words for case conversion: a word is a
|
||||
// run of letters/digits; camelCase humps split; everything else separates.
|
||||
QStringList words(const QString &text)
|
||||
{
|
||||
QStringList out;
|
||||
QString cur;
|
||||
for (int i = 0; i < text.size(); ++i) {
|
||||
const QChar c = text.at(i);
|
||||
const bool isWordChar = c.isLetterOrNumber();
|
||||
if (!isWordChar) {
|
||||
if (!cur.isEmpty()) {
|
||||
out << cur;
|
||||
cur.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// camelCase hump: lower/digit -> upper starts a new word.
|
||||
if (!cur.isEmpty() && c.isUpper() && !cur.at(cur.size() - 1).isUpper()) {
|
||||
out << cur;
|
||||
cur.clear();
|
||||
}
|
||||
cur.append(c);
|
||||
}
|
||||
if (!cur.isEmpty()) {
|
||||
out << cur;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QString sortLines(const QString &text, bool unique, bool caseInsensitive)
|
||||
{
|
||||
bool trailing = false;
|
||||
QStringList lines = splitLines(text, trailing);
|
||||
std::stable_sort(lines.begin(), lines.end(), [caseInsensitive](const QString &a, const QString &b) {
|
||||
return a.compare(b, caseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive) < 0;
|
||||
});
|
||||
if (unique) {
|
||||
auto eq = [caseInsensitive](const QString &a, const QString &b) {
|
||||
return a.compare(b, caseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive) == 0;
|
||||
};
|
||||
lines.erase(std::unique(lines.begin(), lines.end(), eq), lines.end());
|
||||
}
|
||||
return joinLines(lines, trailing);
|
||||
}
|
||||
|
||||
QString reverseLines(const QString &text)
|
||||
{
|
||||
bool trailing = false;
|
||||
QStringList lines = splitLines(text, trailing);
|
||||
std::reverse(lines.begin(), lines.end());
|
||||
return joinLines(lines, trailing);
|
||||
}
|
||||
|
||||
QString convertCase(const QString &text, CaseStyle style)
|
||||
{
|
||||
switch (style) {
|
||||
case CaseStyle::Upper:
|
||||
return text.toUpper();
|
||||
case CaseStyle::Lower:
|
||||
return text.toLower();
|
||||
case CaseStyle::Title: {
|
||||
QString out = text;
|
||||
bool atWordStart = true;
|
||||
for (int i = 0; i < out.size(); ++i) {
|
||||
const QChar c = out.at(i);
|
||||
if (c.isLetterOrNumber()) {
|
||||
out[i] = atWordStart ? c.toUpper() : c.toLower();
|
||||
atWordStart = false;
|
||||
} else {
|
||||
atWordStart = true;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case CaseStyle::Snake:
|
||||
case CaseStyle::Kebab: {
|
||||
const QChar sep = (style == CaseStyle::Snake) ? QLatin1Char('_') : QLatin1Char('-');
|
||||
QStringList ws = words(text);
|
||||
for (QString &w : ws) {
|
||||
w = w.toLower();
|
||||
}
|
||||
return ws.join(sep);
|
||||
}
|
||||
case CaseStyle::Camel: {
|
||||
const QStringList ws = words(text);
|
||||
QString out;
|
||||
for (int i = 0; i < ws.size(); ++i) {
|
||||
QString w = ws.at(i).toLower();
|
||||
if (i > 0 && !w.isEmpty()) {
|
||||
w[0] = w.at(0).toUpper();
|
||||
}
|
||||
out += w;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
QString base64Encode(const QString &text)
|
||||
{
|
||||
return QString::fromLatin1(text.toUtf8().toBase64());
|
||||
}
|
||||
|
||||
QString base64Decode(const QString &text)
|
||||
{
|
||||
const QByteArray decoded = QByteArray::fromBase64(text.toLatin1());
|
||||
return QString::fromUtf8(decoded);
|
||||
}
|
||||
|
||||
QString rot13(const QString &text)
|
||||
{
|
||||
QString out = text;
|
||||
for (int i = 0; i < out.size(); ++i) {
|
||||
const QChar c = out.at(i);
|
||||
if (c >= QLatin1Char('a') && c <= QLatin1Char('z')) {
|
||||
out[i] = QChar(QLatin1Char('a').unicode() + (c.unicode() - QLatin1Char('a').unicode() + 13) % 26);
|
||||
} else if (c >= QLatin1Char('A') && c <= QLatin1Char('Z')) {
|
||||
out[i] = QChar(QLatin1Char('A').unicode() + (c.unicode() - QLatin1Char('A').unicode() + 13) % 26);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString uuid()
|
||||
{
|
||||
return QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
}
|
||||
|
||||
} // namespace texttransforms
|
||||
} // namespace katecustom
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* Pure text transforms used by the Ollie ":" command verbs. Kept free of any
|
||||
* KTextEditor dependency so they are unit testable in isolation; the command
|
||||
* glue in olliecommands.cpp feeds them the selection and writes back the result.
|
||||
*/
|
||||
#ifndef KATECUSTOM_TEXTTRANSFORMS_H
|
||||
#define KATECUSTOM_TEXTTRANSFORMS_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
enum class CaseStyle {
|
||||
Upper,
|
||||
Lower,
|
||||
Title, // Each Word Capitalised
|
||||
Snake, // snake_case
|
||||
Camel, // camelCase
|
||||
Kebab, // kebab-case
|
||||
};
|
||||
|
||||
namespace texttransforms
|
||||
{
|
||||
|
||||
/*! Sort the lines of \a text. If \a unique, drop adjacent duplicates after
|
||||
* sorting. A trailing newline in the input is preserved in the output. */
|
||||
QString sortLines(const QString &text, bool unique = false, bool caseInsensitive = false);
|
||||
|
||||
/*! Reverse the order of the lines of \a text. */
|
||||
QString reverseLines(const QString &text);
|
||||
|
||||
/*! Convert \a text to the requested case style. Word splitting for the
|
||||
* programmer styles (snake/camel/kebab) treats any run of letters/digits as a
|
||||
* word and everything else as a separator; Title/Upper/Lower keep separators. */
|
||||
QString convertCase(const QString &text, CaseStyle style);
|
||||
|
||||
/*! Base64 encode / decode (UTF-8). Decode returns the original on invalid
|
||||
* input only if it cannot be decoded at all; otherwise best-effort. */
|
||||
QString base64Encode(const QString &text);
|
||||
QString base64Decode(const QString &text);
|
||||
|
||||
/*! ROT13 letters, leave everything else untouched. */
|
||||
QString rot13(const QString &text);
|
||||
|
||||
/*! A random version-4 UUID in 8-4-4-4-12 lowercase hex form, no braces. */
|
||||
QString uuid();
|
||||
|
||||
} // namespace texttransforms
|
||||
} // namespace katecustom
|
||||
|
||||
#endif
|
||||
|
|
@ -18,6 +18,7 @@ kcoreaddons_add_plugin(olliepalette
|
|||
target_link_libraries(olliepalette PRIVATE
|
||||
palette
|
||||
radial
|
||||
olliecommands_lib
|
||||
KF6::TextEditor
|
||||
KF6::XmlGui
|
||||
KF6::CoreAddons
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "palettewidget.h"
|
||||
#include "radialmenu.h"
|
||||
#include "radialconfig.h"
|
||||
#include "olliecommands.h"
|
||||
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
|
|
@ -130,6 +131,8 @@ OlliePlugin::OlliePlugin(QObject *parent, const QVariantList &args)
|
|||
: KTextEditor::Plugin(parent)
|
||||
{
|
||||
Q_UNUSED(args);
|
||||
// The ":"-verb pack registers itself with the global editor on construction.
|
||||
m_commands = new OllieCommands(this);
|
||||
}
|
||||
|
||||
OlliePlugin::~OlliePlugin() = default;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace katecustom
|
|||
|
||||
class PaletteWidget;
|
||||
class RadialMenu;
|
||||
class OllieCommands;
|
||||
struct RadialDef;
|
||||
|
||||
class OlliePlugin : public KTextEditor::Plugin
|
||||
|
|
@ -43,6 +44,9 @@ public:
|
|||
~OlliePlugin() override;
|
||||
|
||||
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
|
||||
|
||||
private:
|
||||
OllieCommands *m_commands = nullptr; // ":"-verb pack, registered once globally
|
||||
};
|
||||
|
||||
class OllieView : public QObject
|
||||
|
|
|
|||
Loading…
Reference in New Issue