docs: drop the roadmap, add AGENTS.md
Remove docs/PLAN.md (goal/constraints/milestone roadmap — stale planning prose) and the README's Status/milestone section; the README already documents what the plugins are and do. Refresh the layout tree's docs/ listing accordingly. Add AGENTS.md: a working guide for contributors/agents — the one-.so-per-feature architecture and shared libs, build/test/install commands, naming and style conventions, the two load-bearing design decisions (eventFilter'd launcher keys vs ambiguous shortcuts; cross-plugin calls by action objectName with no link dependency), and the gotchas (osort collision, per-plugin frecency files, live-Kate behaviour being unverifiable in CI).
This commit is contained in:
parent
d9829c9327
commit
812ab782f8
|
|
@ -0,0 +1,114 @@
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Working notes for anyone (human or agent) hacking on **Deft**, a suite of Kate /
|
||||||
|
KTextEditor plugins. Read this before changing code.
|
||||||
|
|
||||||
|
## What this repo is
|
||||||
|
|
||||||
|
Six independent KTextEditor plugins, one `.so` per feature, plus a few shared
|
||||||
|
static libraries. Each plugin is a `KTextEditor::Plugin` with its own
|
||||||
|
`K_PLUGIN_FACTORY_WITH_JSON` factory and `.json` metadata. Kate loads one plugin
|
||||||
|
per `.so`.
|
||||||
|
|
||||||
|
| `.so` | display name | source dir | links |
|
||||||
|
|-------|--------------|------------|-------|
|
||||||
|
| `deft_palette` | `[deft:util] palette` | `src/palette/` | `palette`, `deftcommands_lib` |
|
||||||
|
| `deft_switch` | `[deft:util] switch` | `src/switch/` | `palette`, `project_lib` |
|
||||||
|
| `deft_radial` | `[deft:util] radial` | `src/radial/` | `radial` |
|
||||||
|
| `deft_acme` | `[deft:nineify] acme` | `src/acme/` | self-contained |
|
||||||
|
| `deft_sam` | `[deft:nineify] sam` | `src/sam/` | `sam_lib` |
|
||||||
|
| `deft_plumb` | `[deft:nineify] plumb`| `src/plumb/` | `plumb_lib` |
|
||||||
|
|
||||||
|
Shared static libs (headless, unit-tested in isolation):
|
||||||
|
`fuzzyranker`, `palette` (model + widget + `FrecencyStore`), `deftcommands_lib`
|
||||||
|
(text transforms + the `:`-verb `KTextEditor::Command`), `project_lib`
|
||||||
|
(project/symbol index, `ProjectIndexer`, `KateProjectBridge`), `sam_lib`,
|
||||||
|
`plumb_lib`.
|
||||||
|
|
||||||
|
## Build / test / install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -B build -S . # configure (defaults: RelWithDebInfo, prefix ~/.local)
|
||||||
|
cmake --build build -j # build everything
|
||||||
|
QT_QPA_PLATFORM=offscreen ctest --test-dir build # 18 tests, must stay green
|
||||||
|
cmake --install build # installs the 6 .so to ~/.local/.../qt6/plugins/kf6/ktexteditor
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Always build and run the full `ctest` after a change.** Tests need
|
||||||
|
`QT_QPA_PLATFORM=offscreen` (they create real `KTextEditor` docs/views).
|
||||||
|
- The symbol-switcher path and its tests need **`ctags`** on `PATH`; those tests
|
||||||
|
skip gracefully when it is absent.
|
||||||
|
- Installing is wired for a user-local prefix: see `CMakeLists.txt`. It also
|
||||||
|
drops `~/.config/plasma-workspace/env/99-deft.sh` to put the install dir on
|
||||||
|
`QT_PLUGIN_PATH` (generated from `99-deft.sh.in`). Don't hand-roll install
|
||||||
|
paths — `KDE_INSTALL_PLUGINDIR` is pointed at `QT6_INSTALL_PLUGINS` so plugins
|
||||||
|
land where Kate scans.
|
||||||
|
- An unqualified `cmake -B build` produces an **optimized** build on purpose
|
||||||
|
(`CMAKE_BUILD_TYPE=RelWithDebInfo`); the palette re-rank and indexing are
|
||||||
|
several times slower at `-O0`. See `docs/PERF.md`.
|
||||||
|
|
||||||
|
## Conventions that matter
|
||||||
|
|
||||||
|
- **Namespace `deft`, `Deft`-prefixed names in all code.** The project was once
|
||||||
|
called Ollie; nothing should reintroduce `Ollie`/`ollie` in identifiers,
|
||||||
|
config paths (`~/.config/deft/…`), or id prefixes (`deft:cmd:`, `deft:file:`,
|
||||||
|
`deft:sym:`).
|
||||||
|
- **Match the existing style.** No pointless indirection; file-local helpers
|
||||||
|
duplicated across plugins are preferred over a shared lib for a few trivial
|
||||||
|
functions.
|
||||||
|
- **Never read a file you haven't read, then rewrite from memory.** Use targeted
|
||||||
|
edits. The LSP bridge in this environment is unreliable; prefer the Tree-sitter
|
||||||
|
(`code_*`) and `file_*` tools.
|
||||||
|
|
||||||
|
## Two load-bearing design decisions
|
||||||
|
|
||||||
|
Changing either of these will break the suite in non-obvious ways.
|
||||||
|
|
||||||
|
### 1. Launcher keys are intercepted in an `eventFilter`, not registered shortcuts
|
||||||
|
|
||||||
|
The door keys — palette `Alt+X`, switch `Alt+P`/`Alt+G` — are **not** registered
|
||||||
|
as `QAction` shortcuts. Kate already binds many `Alt+<letter>` combos; a
|
||||||
|
registered duplicate makes Qt refuse to fire *either* and report an "ambiguous
|
||||||
|
shortcut". So each view installs an application `eventFilter`, matches the key
|
||||||
|
itself (scoped to its own window via `isAncestorOf`), and consumes it. The
|
||||||
|
`QAction`s still exist for their `objectName`/label and discoverability, just
|
||||||
|
without a shortcut. `plumb`'s `F2` is the exception — it has no Kate collision,
|
||||||
|
so it is a normal registered shortcut.
|
||||||
|
|
||||||
|
### 2. Cross-plugin calls go through action `objectName`, never direct calls
|
||||||
|
|
||||||
|
The plugins are separate `.so`s with no link dependency on each other. When
|
||||||
|
`palette` offers "Go to File/Symbol", or `radial` has a launcher slice, it
|
||||||
|
resolves the target by `objectName` at runtime and triggers it:
|
||||||
|
|
||||||
|
- `switch` registers window actions `deft_goto_file` and `deft_goto_symbol`.
|
||||||
|
- `palette` registers `deft_command_palette`.
|
||||||
|
- `findActionByName` searches the window's GUI-factory client action collections
|
||||||
|
**and** `window()->actions()` (sibling doors are added with
|
||||||
|
`QWidget::addAction`, not via an XMLGUI client — so the window-actions search
|
||||||
|
is required, not optional).
|
||||||
|
|
||||||
|
If you add a new cross-plugin entry point, register the action under a stable
|
||||||
|
`deft_*` objectName and resolve it the same way. Never `#include` another
|
||||||
|
plugin's header or add a link dependency between plugins.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- The `:`-verb sort is registered as **`osort`**, not `sort`: the KTextEditor
|
||||||
|
command registry drops an entire `Command` object if any one of its names
|
||||||
|
collides with a built-in, and Kate ships `sort`. `test_deftregistration`
|
||||||
|
guards against reintroducing a colliding name. `runVerb` still accepts bare
|
||||||
|
`sort` for palette entries.
|
||||||
|
- Each plugin keeps its **own** `FrecencyStore` file (`deft/palette.frecency.json`,
|
||||||
|
`deft/switch.frecency.json`) — separate files, so two independent rankings
|
||||||
|
can't clobber one another through a shared file.
|
||||||
|
- Live Kate behaviour (plugin discovery, the Plasma env snippet pickup, actual
|
||||||
|
gesture handling) can only be verified in a real KDE session. CI/sandbox runs
|
||||||
|
cannot confirm it — flag anything that depends on it as unverified.
|
||||||
|
|
||||||
|
## Reference docs
|
||||||
|
|
||||||
|
- `docs/PERF.md` — switcher performance analysis and the fixes applied.
|
||||||
|
- `docs/PLUMBING.md` — configuring the Plan 9 plumber to route into Kate.
|
||||||
|
- `docs/SAM.md` — the sam structural-regexp dialect and semantics.
|
||||||
|
- `docs/radials.example.json` — example radial-menu config.
|
||||||
32
README.md
32
README.md
|
|
@ -244,30 +244,6 @@ Make Kate a first-class *plumb* client.
|
||||||
- If no plumber is running, it falls back to opening the target directly (URLs
|
- If no plumber is running, it falls back to opening the target directly (URLs
|
||||||
via the system handler, `path[:line]` in Kate).
|
via the system handler, `path[:line]` in Kate).
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
See [docs/PLAN.md](docs/PLAN.md) for the full roadmap.
|
|
||||||
|
|
||||||
- **Milestone 1 — FuzzyRanker (keystone): DONE.** Orderless, layered scoring
|
|
||||||
(exact / substring / word-initials / subsequence / bounded typo), merged
|
|
||||||
highlight ranges. 14 unit tests, all green.
|
|
||||||
- **Milestone 2 — custom palette widget: DONE.** `PaletteModel` +
|
|
||||||
`PaletteWidget` (frameless popup, live filtering, highlighted matches,
|
|
||||||
keyboard + mouse activation). 10 model unit tests, all green.
|
|
||||||
- Milestone 3 — KTextEditor plugin + M-x: **DONE.** `Alt+X` opens the palette
|
|
||||||
over every action in the window's GUI clients.
|
|
||||||
- Milestone 4 — radial caret menu: **DONE (first increment).** Multi-level
|
|
||||||
radial menu at the caret (`Alt+R`) or at the pointer via a **right-mouse
|
|
||||||
gesture**, mouse-driven selection, config tree with per-radial key + mouse
|
|
||||||
triggers. Free-form stroke gestures next.
|
|
||||||
- Milestone 5 — command vocabulary + project-as-directory + switchers: **DONE.**
|
|
||||||
`:`-verb pack, project file (`Alt+P`) and symbol (`Alt+G`) switchers over an
|
|
||||||
async, cached index.
|
|
||||||
- Milestone 6 — one `.so` per feature: **DONE.** The monolithic plugin is split
|
|
||||||
into six independent KTextEditor plugins (`palette`, `switch`, `radial`,
|
|
||||||
`acme`, `sam`, `plumb`), tagged `[deft:util]` / `[deft:nineify]`, cooperating
|
|
||||||
by resolving each other's actions by object name at runtime.
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
Shared building blocks are static libraries (`fuzzy`, `palette`, `project`,
|
Shared building blocks are static libraries (`fuzzy`, `palette`, `project`,
|
||||||
|
|
@ -311,5 +287,11 @@ src/
|
||||||
plumb/
|
plumb/
|
||||||
plumb* + plumbplugin [deft:nineify] plumb — Plan 9 plumber
|
plumb* + plumbplugin [deft:nineify] plumb — Plan 9 plumber
|
||||||
docs/
|
docs/
|
||||||
PLAN.md goal, constraints, roadmap
|
PERF.md switcher performance notes
|
||||||
|
PLUMBING.md configuring the Plan 9 plumber for Kate
|
||||||
|
SAM.md the sam structural-regexp dialect
|
||||||
|
radials.example.json example radial-menu config
|
||||||
|
img/ screenshots used in this README
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For contributing or working on the code, see [AGENTS.md](AGENTS.md).
|
||||||
|
|
|
||||||
449
docs/PLAN.md
449
docs/PLAN.md
|
|
@ -1,449 +0,0 @@
|
||||||
# Plan — kate-custom
|
|
||||||
|
|
||||||
> **Architecture note (current):** The suite has been split into **one `.so` per
|
|
||||||
> feature** — `deft_palette`, `deft_switch`, `deft_radial`, `deft_acme`,
|
|
||||||
> `deft_sam`, `deft_plumb` — each a standalone `KTextEditor::Plugin` tagged
|
|
||||||
> `[deft:util]` / `[deft:nineify]`. See the README's plugin table for the
|
|
||||||
> current layout. The design prose below predates that split and still refers to
|
|
||||||
> the former monolith (`OlliePlugin` / `OllieView`, a single `olliepalette.so`,
|
|
||||||
> and the `olliepalette/` config dir); read those names as historical. In the
|
|
||||||
> split suite the palette plugin owns the M-x launcher and the `:`-verbs, the
|
|
||||||
> switch plugin owns the file/symbol switchers and the project index, each plugin
|
|
||||||
> keeps its own `FrecencyStore` file under `deft/`, and the doors cooperate by
|
|
||||||
> resolving each other's actions by object name at runtime rather than by direct
|
|
||||||
> calls or link dependencies.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Build a suite of Kate plugins that make the editor usable without menus, tuned
|
|
||||||
to one specific operator profile:
|
|
||||||
|
|
||||||
- mouse-heavy, with the mouse used at the caret rather than at the menubar
|
|
||||||
- one-handed keyboard typing
|
|
||||||
- no complex chords; single modifier at most
|
|
||||||
- compact keyboard where F-keys/nav clusters sit behind a layer toggle
|
|
||||||
- long Emacs history and some Sublime Text; `M-x` and type-to-filter palettes
|
|
||||||
are familiar and welcome
|
|
||||||
|
|
||||||
The spine of the suite is a single **action registry** reached through three
|
|
||||||
interchangeable doors: `M-x` (keyboard), a radial caret menu (mouse), and the
|
|
||||||
`:` command line (optional). Every feature registers its commands once and
|
|
||||||
becomes reachable from all three. No action depends on an F-key or a chord.
|
|
||||||
|
|
||||||
### Guiding principle: natural actions drive editing
|
|
||||||
|
|
||||||
Each door suits a different kind of action, and placing an action in the wrong
|
|
||||||
door breaks flow:
|
|
||||||
|
|
||||||
- **Radial** is for actions that **complete in place** — cut, copy, paste,
|
|
||||||
comment, case changes, join lines. The gesture starts and ends at the cursor;
|
|
||||||
attention never leaves the text.
|
|
||||||
- Actions that **open a panel demanding immediate focus elsewhere** (Find,
|
|
||||||
Replace, goto) do **not** belong in the radial. Being flung to a panel right
|
|
||||||
after a cursor gesture is jarring. These belong in the **command palette** or
|
|
||||||
a **keybind**, where the context switch is expected. The radial keeps a
|
|
||||||
"Command Palette…" slice as the escape hatch to them.
|
|
||||||
|
|
||||||
## Why a custom matcher is the keystone
|
|
||||||
|
|
||||||
Kate's built-in command bar cannot be fixed in place:
|
|
||||||
|
|
||||||
- `KCommandBar` (KConfigWidgets) exposes only `setActions()`. There is no hook
|
|
||||||
to replace or configure its matcher.
|
|
||||||
- `KFuzzyMatcher` (KCoreAddons) is single-needle, strictly in-order
|
|
||||||
subsequence, and not typo tolerant. KDE's own docs state `"gti"` will not
|
|
||||||
match `"git"`. No orderless, no company-style completion.
|
|
||||||
|
|
||||||
Therefore the palette is replaced, not extended, and the replacement is built on
|
|
||||||
`FuzzyRanker`, a matcher with:
|
|
||||||
|
|
||||||
- **orderless** tokenization (space-separated needles matched in any order)
|
|
||||||
- **layered scoring**, strongest tier first:
|
|
||||||
1. exact
|
|
||||||
2. substring
|
|
||||||
3. word-boundary initials (`rs` -> **R**ename **S**ymbol)
|
|
||||||
4. in-order subsequence
|
|
||||||
5. bounded typo via a single adjacent transposition (`gti` -> `git`;
|
|
||||||
deliberately narrow to avoid false positives like `ren` -> "Open File")
|
|
||||||
- **merged highlight ranges** for the UI
|
|
||||||
- AND semantics across needles, case-folded, shorter-candidate preference
|
|
||||||
|
|
||||||
## Milestones
|
|
||||||
|
|
||||||
### M1 — FuzzyRanker (keystone) — DONE
|
|
||||||
- Repo scaffold: top-level CMake (KF6/Qt6/ECM), `src/`, `src/fuzzy/`.
|
|
||||||
- `src/fuzzy/fuzzyranker.{h,cpp}`: tokenize + layered scoring + ranges.
|
|
||||||
- `src/fuzzy/test_fuzzyranker.cpp`: QTest suite, 14 cases, all green,
|
|
||||||
including `gti -> git` and `ren sym -> Rename Symbol`.
|
|
||||||
|
|
||||||
### M2 — Palette widget (replaces KCommandBar) — DONE
|
|
||||||
- `PaletteModel` (`src/palette/palettemodel.{h,cpp}`): `QAbstractListModel`,
|
|
||||||
re-ranks via `FuzzyRanker` on `setQuery`, exposes id/label/group/score and
|
|
||||||
highlight ranges per row, frecency bonus. No widget dependency.
|
|
||||||
- `PaletteWidget` (`src/palette/palettewidget.{h,cpp}`): frameless popup, filter
|
|
||||||
line + results list, live company-style updates, top hit preselected, custom
|
|
||||||
delegate bolds matched ranges. Keyboard: Up/Down/PageUp/PageDown, Enter
|
|
||||||
activates, Esc cancels — cursor stays in the filter line, no chords. Mouse:
|
|
||||||
click a row to activate. Emits `activatedId(id)` / `cancelled()`.
|
|
||||||
- `src/palette/test_palettemodel.cpp`: 10 QTest cases, all green (filtering,
|
|
||||||
orderless, typo, best-first ordering, frecency tie-break, highlight ranges).
|
|
||||||
|
|
||||||
### M3 — KTextEditor plugin + M-x + action registry — DONE
|
|
||||||
- `src/plugin/ollieplugin.{h,cpp}` + `ollieplugin.json`: a loadable
|
|
||||||
`KTextEditor::Plugin` (`olliepalette.so`) built with `kcoreaddons_add_plugin`.
|
|
||||||
- `OllieView` (one per MainWindow) provides the **M-x** launcher on `Alt+X`.
|
|
||||||
LAUNCHER KEYS ARE NOT QAction SHORTCUTS: `Alt+X` (M-x), `Alt+P` (Go to File)
|
|
||||||
and `Alt+G` (Go to Symbol) are intercepted in `eventFilter()` (accept the
|
|
||||||
`ShortcutOverride`, act on the `KeyPress`), scoped to this plugin's window.
|
|
||||||
Registering them as `ApplicationShortcut` QActions clashed with Kate's own
|
|
||||||
`Alt+<letter>` bindings and Qt refused to fire either ("ambiguous shortcut").
|
|
||||||
The event-filter route cannot be ambiguous because nothing is registered —
|
|
||||||
the same technique already used for the Acme line-editing keys. The QActions
|
|
||||||
still exist (objectName/label) for command-palette harvest and as radial
|
|
||||||
escape-hatch slices (`ollie_command_palette` / `ollie_goto_file` /
|
|
||||||
`ollie_goto_symbol`, handled directly in `onRadialActivated`).
|
|
||||||
- On trigger it aggregates every enabled, visible `QAction` from the window's
|
|
||||||
`guiFactory()->clients()` action collections, keyed by stable `objectName`,
|
|
||||||
and feeds them to `PaletteWidget`. The action's current shortcut is shown in
|
|
||||||
the group column. Accepting a row triggers the underlying `QAction`.
|
|
||||||
- Installs to `kf6/ktexteditor`; metadata verified valid via `KPluginMetaData`.
|
|
||||||
- Frecency: wired. `OllieView` holds a persisted `FrecencyStore`; activations
|
|
||||||
are recorded and `PaletteItem::frecency` is populated for every palette (see
|
|
||||||
the Frecency entry under M5).
|
|
||||||
- Duplicate handling: actions are deduplicated only by pointer identity (the
|
|
||||||
exact same action object reachable through several GUI clients is listed
|
|
||||||
once — functionally lossless). Distinct actions are never dropped. When two
|
|
||||||
distinct actions would render with the same label, the label is enriched
|
|
||||||
through a fallback chain until every visible row is unique: component name →
|
|
||||||
objectName → shortcut → numeric suffix. Each entry also gets a unique id so
|
|
||||||
activation always triggers the intended action.
|
|
||||||
|
|
||||||
### M4 — Radial caret menu (mouse door) — DONE (first increment)
|
|
||||||
- `src/radial/radialmodel.{h,cpp}`: pure tree + geometry. Multi-level
|
|
||||||
(branch/leaf) `RadialNode` tree, angle hit-testing (0 = up, clockwise), inner
|
|
||||||
dead zone, far-flick snaps by angle, descend/ascend navigation. 14 tests.
|
|
||||||
- `src/radial/radialmenu.{h,cpp}`: frameless translucent `QWidget`; paints the
|
|
||||||
current ring (wedges, labels, branch ▸, central "back" hub), highlights the
|
|
||||||
slice under the pointer. Two interaction modes from one widget: a **drag
|
|
||||||
gesture** (opened with a button held — drag out and release to select; a
|
|
||||||
release that never left the dead zone switches to click mode and keeps the
|
|
||||||
menu open) and a **discrete click mode** (click a slice to select, click
|
|
||||||
outside the ring to cancel). Esc ascends/cancels. Opens centred on the given
|
|
||||||
point without moving the caret.
|
|
||||||
- `src/radial/radialconfig.{h,cpp}`: JSON → radial tree parser (+ built-in
|
|
||||||
default) so multiple radials can be bound to different triggers. 8 tests.
|
|
||||||
- Wired into the plugin: `OllieView::setupRadials()` registers a key trigger
|
|
||||||
(default **Alt+R**) and a **mouse-button gesture** (default **right mouse
|
|
||||||
button**) per configured radial. Because the opening RMB press leaves an
|
|
||||||
implicit grab on the editor view, the whole gesture (hover + release) is
|
|
||||||
driven from the plugin's application event filter in global coordinates
|
|
||||||
(`driveHoverGlobal`/`driveReleaseGlobal`), not from a popup mouse grab — this
|
|
||||||
fixes stale hover and wrong-slice selection. The press and the following
|
|
||||||
`ContextMenu` are consumed, so the context menu is suppressed and the caret
|
|
||||||
does not move. Key-triggered radials pop at the caret
|
|
||||||
(`View::cursorPositionCoordinates()`) and use the widget's own click handling.
|
|
||||||
- A slice may reference any Kate action by objectName, resolved via
|
|
||||||
`findActionByName`; the special `ollie_command_palette` slice opens the palette.
|
|
||||||
- Honours the agreed model: radials are triggerable by keys (weapon-wheel style)
|
|
||||||
and by mouse buttons/gestures; the config carries both a key `trigger` and a
|
|
||||||
mouse `button` per radial.
|
|
||||||
- Not yet done: free-form stroke gestures (only button-press gesture so far).
|
|
||||||
- Built-in radials: an **edit** radial (Alt+R / right button) of in-place
|
|
||||||
actions, and a **window** radial (**middle button**) for split management
|
|
||||||
(`view_split_vert|horiz`, `view_close_current_space|others`,
|
|
||||||
`view_split_toggle`, and a "Focus" sub-branch of `go_*_split_view`). A plain
|
|
||||||
middle press (no left button held) routes to this radial; the Acme chords are
|
|
||||||
all **left+something**, so a middle press that is part of a chord
|
|
||||||
(left held) still cuts and never opens the radial. The X11 middle-click
|
|
||||||
primary paste is suppressed on release either way.
|
|
||||||
- Cursor-follows-focus: after a radial `go_*_split_view` slice (the window
|
|
||||||
radial's "Focus" sub-branch) the mouse pointer warps onto the newly active
|
|
||||||
pane. Kate's split panes are child widgets of one top-level window (one
|
|
||||||
`wl_surface`), so this is a warp *within* that surface — pure geometry
|
|
||||||
(`view->mapTo(topLevel, view->rect().center())`). On **X11** it uses
|
|
||||||
`QCursor::setPos`. On **Wayland** (which forbids that) it uses the
|
|
||||||
`zwp_pointer_constraints_v1` protocol: lock the pointer to the window surface,
|
|
||||||
`set_cursor_position_hint`, commit, then release — KWin warps to the hint on
|
|
||||||
unlock (the mechanism plan9port/acme use). Implemented in
|
|
||||||
`src/plugin/waylandcursorwarp.{h,cpp}`; the pointer-constraints client glue is
|
|
||||||
generated by ECM (`ecm_add_wayland_client_protocol`) and compiled as a small
|
|
||||||
C static lib (`kcoreaddons_add_plugin` drops `.c` sources). Everything is
|
|
||||||
guarded: no Wayland global / surface → `warp()` returns false and the caller
|
|
||||||
falls back. The feature compiles out entirely if wayland-scanner /
|
|
||||||
wayland-client / Qt6 GuiPrivate are absent (a stub provides the symbols).
|
|
||||||
- User-file radial config — DONE. `RadialConfig::load()` reads
|
|
||||||
`$XDG_CONFIG_HOME/olliepalette/radials.json` when present and non-empty,
|
|
||||||
otherwise returns `builtinDefault()`; a present-but-broken file also falls
|
|
||||||
back (never leaves the user with no radials). `userConfigPath()` and
|
|
||||||
`loadFile(path, &error)` are exposed for callers/tests. `setupRadials()` now
|
|
||||||
calls `load()`. Example config at `docs/radials.example.json`. 4 added tests
|
|
||||||
(valid file, missing, invalid JSON, non-empty fallback).
|
|
||||||
|
|
||||||
### Mouse chording (Acme-style) — DONE
|
|
||||||
- Left-button chords (button 1 held while selecting is the anchor):
|
|
||||||
**left + middle = Cut**, **left + right = Paste**. Both use `edit_cut` /
|
|
||||||
`edit_paste` → the **standard KDE clipboard** (Klipper-backed), not the X11
|
|
||||||
primary selection.
|
|
||||||
- The right-button paste chord is free of conflict because the radial gesture is
|
|
||||||
already suppressed while the left button is held: right alone = radial, right
|
|
||||||
while left-held = paste.
|
|
||||||
- Cut/Copy/Paste were removed from the radial once the chords covered them —
|
|
||||||
the radial should not duplicate what a more in-flow gesture already does.
|
|
||||||
- **Middle-click primary paste is suppressed** inside the editor — the middle
|
|
||||||
button is consumed on both press and release (the paste fires on release, so
|
|
||||||
swallowing only the press leaked it). Paste in the editor is the KDE-clipboard
|
|
||||||
paste (`Ctrl+V`, palette, or the left+right chord).
|
|
||||||
|
|
||||||
### Bracket-pair double-click selection — DONE
|
|
||||||
- Double-clicking next to a bracket (`()[]{}`) selects the text strictly between
|
|
||||||
the matching pair, **multi-line aware** and nesting-aware. Adjacency is checked
|
|
||||||
on both sides of the click; openers select forward, closers select backward.
|
|
||||||
- Double-clicking next to a quote (`''`, `""`, `` `` ``) selects the text strictly
|
|
||||||
between the enclosing pair. Quotes are symmetric and non-nesting, so matching is
|
|
||||||
**line-local**: direction is decided by the parity of same-kind quotes preceding
|
|
||||||
the clicked one (even → opener, scan forward; odd → closer, scan backward), via
|
|
||||||
`isQuote` / `quoteIsOpener` / `findQuoteForward` / `findQuoteBackward`.
|
|
||||||
- Implemented in the event filter via `selectBracketPairAt`: maps the click to a
|
|
||||||
document cursor (`coordinatesToCursor`), reads neighbouring characters
|
|
||||||
(`characterAt`), scans with depth counting across line ends, and sets the
|
|
||||||
selection. When no bracket or quote is adjacent it falls through to Kate's
|
|
||||||
default double-click (word select).
|
|
||||||
- A left double-click leaves the button held, so it also arms the chord anchor:
|
|
||||||
the user can go straight from a double-click (bracket- or word-select) into a
|
|
||||||
left+middle / left+right chord without releasing.
|
|
||||||
|
|
||||||
### Acme line-editing keys — DONE
|
|
||||||
- Keys bound (overriding Kate/KDE defaults, `ApplicationShortcut`):
|
|
||||||
**C-a** beginning of line, **C-e** end of line, **C-h** erase char back,
|
|
||||||
**C-u** erase to line start, **C-w** erase word back. Semantics ported from
|
|
||||||
plan9port acme `text.c` (`^A`/`^U` stop at the line's newline; `^W` skips
|
|
||||||
non-alphanumerics then eats the alphanumeric run; isalnum excludes `_`).
|
|
||||||
- Implemented directly against the View/Document API (`cursorPosition`,
|
|
||||||
`setCursorPosition`, `removeText`, `characterAt`, `lineLength`).
|
|
||||||
- Bound via the application **event filter**, not QActions: QActions for
|
|
||||||
`Ctrl+A/W/…` collide with Kate's own (Select All, Close) and Qt reports
|
|
||||||
ambiguous shortcuts. Instead the filter accepts the `ShortcutOverride` for
|
|
||||||
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 — DONE
|
|
||||||
- `:`-verb pack — DONE (palette **and** `:` command line). `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}`. The verb logic lives in
|
|
||||||
`OllieCommands::runVerb(view, cmd, msg)` and transforms the selection in place
|
|
||||||
(or the whole document when nothing is selected); `pipe <shell>` runs
|
|
||||||
`sh -c <shell>` with the selection on stdin.
|
|
||||||
- Verbs are invoked from the **M-x palette** (entries prefixed `ollie:cmd:`,
|
|
||||||
executed by calling `OllieCommands::runVerb` directly). `case` is expanded
|
|
||||||
into one entry per style; `pipe` is palette-omitted (needs free text).
|
|
||||||
- They are **also reachable from Kate's `:` command line**. `OllieCommands`
|
|
||||||
is a `KTextEditor::Command`; its base constructor auto-registers its names
|
|
||||||
at the global `Editor::instance()`, and `exec()` forwards to `runVerb`.
|
|
||||||
- ROOT-CAUSE FIX of the earlier "not reachable from `:`" problem: the
|
|
||||||
KTextEditor registry **aborts registering a Command object entirely if any
|
|
||||||
one of its names collides** with an already-registered command. Our list
|
|
||||||
led with `sort`, which Kate ships as a built-in, so the collision silently
|
|
||||||
dropped **all** our verbs. The sort verb is now registered as **`osort`**
|
|
||||||
(runVerb still accepts the bare `sort`, so palette entries are unchanged).
|
|
||||||
`test_ollieregistration` asserts every verb resolves to our object via
|
|
||||||
`Editor::queryCommand`/`commandList` and guards against reclaiming `sort`.
|
|
||||||
- A live test (`test_olliecommands_live`, headless KTextEditor doc/view)
|
|
||||||
proves the transforms mutate a real buffer.
|
|
||||||
- Project = directory ("open folder == open project"): DONE (explicit + core +
|
|
||||||
switcher). `src/project/projectindex.{h,cpp}` (`project_lib`, Qt::Core only,
|
|
||||||
15 unit tests):
|
|
||||||
- `findRoot(startDir)` walks up for a VCS marker (`.git`/`.hg`/`.svn`) and
|
|
||||||
returns that ancestor, else the start directory itself — so any folder is a
|
|
||||||
usable project with zero config.
|
|
||||||
- `resolveRoot(explicitRoot, startDir)` is the effective-root decision: an
|
|
||||||
existing explicit pick is honoured **exactly** (no walk-up — VSCode/Sublime
|
|
||||||
"open folder" semantics); otherwise it derives via `findRoot(startDir)` with
|
|
||||||
cwd as the final fallback. A stale explicit root reverts to discovery.
|
|
||||||
- Explicit folder-as-project is **not** reimplemented: Kate's project plugin
|
|
||||||
already opens folders and shows a file tree, so we build on it rather than
|
|
||||||
duplicate it (see `KateProjectBridge` below).
|
|
||||||
- `KateProjectBridge` (`src/project/kateprojectbridge.{h,cpp}`, in
|
|
||||||
`project_lib`, 7 unit tests) reads Kate's built-in `kateprojectplugin` view
|
|
||||||
through the **meta-object system** — `projectBaseDir` / `projectName` /
|
|
||||||
`projectFiles` properties and the `projectMapChanged` /
|
|
||||||
`projectFileNameChanged` / `pluginProjectAdded|Removed` signals (relayed to
|
|
||||||
a single `projectChanged()`), with **no link dependency**. `OllieView`
|
|
||||||
attaches it on construction and follows `MainWindow::pluginViewCreated` /
|
|
||||||
`pluginViewDeleted`. Tested headlessly against a stand-in QObject that
|
|
||||||
mirrors the plugin's property/signal surface.
|
|
||||||
- `currentProjectRoot()` is a **two-tier** resolution:
|
|
||||||
`KateProjectBridge::baseDir()` (Kate's loaded project) →
|
|
||||||
`ProjectIndex::resolveRoot("", activeDocDir)` (VCS discovery, cwd fallback).
|
|
||||||
- `listFiles(root)` prefers `git ls-files --cached --others
|
|
||||||
--exclude-standard` inside a git work tree (honours .gitignore, includes
|
|
||||||
untracked-not-ignored), and falls back to a bounded recursive walk
|
|
||||||
(`walkDirectory`) that skips noise dirs (node_modules, build, target, …) and
|
|
||||||
symlinks when there is no git (or git yields nothing).
|
|
||||||
- Wired into the plugin: **Alt+P** "Go to File (project)" opens a second
|
|
||||||
PaletteWidget populated with the project's files. The **full project-relative
|
|
||||||
path** is the matched/displayed label, so FuzzyRanker scores across the
|
|
||||||
whole path (orderless: "palette model" matches `src/palette/palettemodel.cpp`)
|
|
||||||
and highlights matched characters along it; a base-name hit still ranks first
|
|
||||||
because `/` is a word boundary and the length penalty favours shorter paths.
|
|
||||||
The root is `currentProjectRoot()` (Kate's loaded project, else derived).
|
|
||||||
Activation opens the file via `MainWindow::openUrl` + `activateView`. Also
|
|
||||||
reachable from the M-x palette (`ollie:switch:file`) and, by objectName
|
|
||||||
`ollie_goto_file`, from a radial. (2 path-ranking tests in
|
|
||||||
`test_palettemodel`.)
|
|
||||||
- PERFORMANCE: file/symbol indexing runs **off the UI thread** via
|
|
||||||
`ProjectIndexer` (QtConcurrent, cached per root, invalidated on
|
|
||||||
`projectChanged`); the palette opens instantly and fills in. The palette
|
|
||||||
re-rank is **incremental** on type-forward (see `PaletteModel`). See
|
|
||||||
`docs/PERF.md` for measured numbers.
|
|
||||||
- Fuzzy symbol switcher scoped to the folder: DONE.
|
|
||||||
`src/project/symbolindex.{h,cpp}` (in `project_lib`, 7 unit tests incl. a
|
|
||||||
live ctags run):
|
|
||||||
- System ctags is **Exuberant 5.9** (no JSON), so `parseTags` parses the
|
|
||||||
classic extended tab format from `ctags -f - -L - --fields=+nK
|
|
||||||
--excmd=number --sort=no` (file list fed on stdin so a big project never
|
|
||||||
overflows ARG_MAX; paths relative to the root). Each `Symbol` carries
|
|
||||||
name / file / line / kind / scope.
|
|
||||||
- Wired into the plugin: **Alt+G** "Go to Symbol (project)" opens a third
|
|
||||||
PaletteWidget of the project's symbols. The label is
|
|
||||||
`Scope::name — kind file` — the name leads (so a name hit ranks first)
|
|
||||||
while the kind and file are both visible and filterable in the same query
|
|
||||||
(e.g. "method olliecommands"). Activation opens the file via
|
|
||||||
`MainWindow::openUrl` and jumps the cursor to the symbol's line (ctags
|
|
||||||
1-based → cursor 0-based). Also in the M-x palette (`ollie:switch:symbol`)
|
|
||||||
and reachable from a radial by objectName `ollie_goto_symbol`.
|
|
||||||
(Alt+R is reserved for the radial key trigger, so this uses Alt+G.)
|
|
||||||
- Frecency (usage-aware ranking) — DONE.
|
|
||||||
`src/palette/frecencystore.{h,cpp}` (in `palette`, Qt::Core only, 11 unit
|
|
||||||
tests):
|
|
||||||
- Per-id `{count, last-used}` persisted as flat JSON (via `QSaveFile`) at
|
|
||||||
`$XDG_CONFIG_HOME/olliepalette/frecency.json`. `bonus(id, now)` combines a
|
|
||||||
recency bucket (<1d=100, <1w=70, <1mo=50, <3mo=30, else 10) with a visit
|
|
||||||
count capped at 10 → an integer in [0,100]. The cap keeps the bonus well
|
|
||||||
below a textual-match tier (substring=400) so frecency nudges ordering and
|
|
||||||
breaks ties without overriding a clearly better match; on an empty query it
|
|
||||||
floats habitual choices to the top.
|
|
||||||
- Wired into the plugin: `OllieView` loads the store in its ctor, every
|
|
||||||
activation (`runAction` for the M-x palette; the file/symbol switcher
|
|
||||||
lambdas) calls `recordUsage` (bump + save), and each palette sets
|
|
||||||
`PaletteItem::frecency` from `bonus()` before showing. File/symbol keys are
|
|
||||||
**project-qualified** (prefixed by the resolved root) so usage never bleeds
|
|
||||||
between repos that share relative paths.
|
|
||||||
|
|
||||||
### Plumbing — plan9 "plumb to edit", full client — DONE
|
|
||||||
Kate is a first-class plan9port plumb client: it both **sends** plumb messages
|
|
||||||
and **receives** them on the `edit` port, so files plumbed from anywhere (acme,
|
|
||||||
`plumb(1)`, other tools) open in Kate at the right line, and tokens plumbed from
|
|
||||||
Kate flow through the real plumber rules.
|
|
||||||
|
|
||||||
- **Native C++ 9P2000** (`src/plumb/ninep.{h,cpp}`): a minimal synchronous
|
|
||||||
client over `QLocalSocket` — `Tversion`/`Tattach`/`Twalk`/`Topen`/`Tread`/
|
|
||||||
`Twrite`/`Tclunk`, little-endian, msize negotiated from 8192. No linking of
|
|
||||||
plan9port's C `libplumb`/`lib9pclient`; it's just the wire protocol, verified
|
|
||||||
against `/usr/local/plan9` sources. For the blocking edit-port read it adds a
|
|
||||||
**cancellable split** (`beginRead` + `recvReadReply` with a first-byte
|
|
||||||
timeout) so the reader thread polls its stop flag between windows and never
|
|
||||||
issues a second `Tread` on the same tag, and never touches the socket from
|
|
||||||
another thread.
|
|
||||||
- **Message codec** (`src/plumb/plumbmsg.{h,cpp}`): `PlumbMsg::pack/unpack` with
|
|
||||||
libplumb's exact attribute quoting (`'`-quote values with space/`'`/`=`/tab,
|
|
||||||
`''` escapes `'`). Unit tests pin it to **golden bytes captured from a live
|
|
||||||
plumber** on the edit port, e.g. `plumb\nedit\n/tmp\ntext\naddr=2\n18\n/tmp/
|
|
||||||
plumbtest.txt`.
|
|
||||||
- **Address parsing** (`src/plumb/plumbresolve.{h,cpp}`): plan9 `addr`
|
|
||||||
(`N`, `N:C`, `N.C`, 1-based) → 0-based `KTextEditor::Cursor`. Unit-tested.
|
|
||||||
- **Facade** (`src/plumb/plumber.{h,cpp}`): `Plumber::send(data, wdir)` opens the
|
|
||||||
`send` port and writes a packed message (`src=kate`, empty `dst` so the rules
|
|
||||||
route it); a `PlumbReader` `QThread` holds the `edit` port open and emits
|
|
||||||
`edit(file, addr, wdir)` via a queued signal. Namespace socket resolved as
|
|
||||||
`$NAMESPACE/plumb`, else `/tmp/ns.$USER.$DISPLAY/plumb` (same as plan9
|
|
||||||
`getns()`), display canonicalised.
|
|
||||||
- **Plugin wiring** (`src/plugin/ollieplugin.cpp`): an `ollie_plumb` QAction on
|
|
||||||
**F2** (testing trigger; a function key, so no collision with the Alt+letter
|
|
||||||
door policy — final gesture will be RMB). `plumbAtCursor()` plumbs the
|
|
||||||
selection, else the acme-style file-path token under the caret
|
|
||||||
(`fileTokenAt` + `isFileChar`, matching plan9port acme's `isfilec`: alnum,
|
|
||||||
`_`, and `. - + / : @`; `:` ends the file name and only a digit-led
|
|
||||||
`:line[:col]` suffix is kept), with the active doc's dir
|
|
||||||
as `wdir`; on send failure it falls back to an internal resolver (URL →
|
|
||||||
`QDesktopServices`, path[:line] → open here). Incoming `edit` messages are
|
|
||||||
opened via `MainWindow::openUrl` + `activateView` + `setCursorPosition`; a
|
|
||||||
plumbed path that resolves to a **directory** opens as a folder in Kate
|
|
||||||
instead (relaunches the hosting kate binary on the dir — Kate is
|
|
||||||
single-instance, so it opens in the running window; no in-process folder-open
|
|
||||||
API exists, so this is the documented command-line path).
|
|
||||||
- **Verification**: `test_plumbmsg` + `test_plumbresolve` run headless;
|
|
||||||
`test_plumb_live` does a **real round-trip** against a running plumber (send
|
|
||||||
`<file>:2` → rules → edit port → `edit()` signal) and **skips+passes** when no
|
|
||||||
plumber/namespace is reachable, so CI stays green. The live round-trip was
|
|
||||||
confirmed passing. The GUI open/cursor path (`openUrl`/`setCursorPosition`)
|
|
||||||
is not exercisable headless and is unverified on a live display.
|
|
||||||
- **User configuration**: routing (what a plumbed string means, and that files
|
|
||||||
and directories open in Kate) is set in `~/lib/plumbing`. See
|
|
||||||
**`docs/PLUMBING.md`** for the editor variable, the directory rule, reloading
|
|
||||||
the plumber, and troubleshooting.
|
|
||||||
|
|
||||||
### Sam — structural-regexp editing panel — DONE
|
|
||||||
A dockable tool view ("Sam", left sidebar) runs the plan9 **sam** command
|
|
||||||
language against the active document, or project-wide via `X`/`Y`. Type a
|
|
||||||
program, click **Run** (or Ctrl+Return); each Run is one undo step.
|
|
||||||
|
|
||||||
- **Regex engine** (`src/sam/samregex.{h,cpp}`, 18 unit tests): a faithful port
|
|
||||||
of sam's own matcher (plan9port `src/cmd/sam/regexp.c`) — a Thompson/Pike NFA.
|
|
||||||
**Leftmost-longest** (POSIX), so it matches real sam exactly incl. overlapping
|
|
||||||
alternation (`a|ab` → `ab`); **linear time**, no backtracking (immune to
|
|
||||||
`(a*)*b` blowup). sam dialect only: `. * + ? | ( ) [ ] ^ $ \` with `\n`; `^`/`$`
|
|
||||||
are per-line and `.`/negated-classes exclude newline, all intrinsic. No PCRE
|
|
||||||
extras — not a deviation, just sam. Replaces the earlier QRegularExpression
|
|
||||||
matcher, eliminating the greedy-vs-longest divergence entirely.
|
|
||||||
|
|
||||||
- **Pure engine** (`src/sam/samengine.{h,cpp}`, unit-tested, no Kate dep):
|
|
||||||
`SamEngine::run(program, text, dotStart, dotEnd) -> SamResult{edits, dot,
|
|
||||||
output, applied}`. Edits are computed against the ORIGINAL snapshot in
|
|
||||||
character offsets (sam computes all of a command's change addresses in the
|
|
||||||
original file — see sam(1) "Grouping and multiple changes") and returned
|
|
||||||
non-overlapping and sorted. A hand-written recursive-descent parser +
|
|
||||||
evaluator mirrors plan9port `src/cmd/sam` (`cmd.c` command table, `address.c`
|
|
||||||
address eval incl. `lineaddr`/`charaddr`, `xec.c` `s_cmd`/`looper`).
|
|
||||||
- **Language**: addresses `#n`, `n`, `0`, `$`, `.`, `'` (k mark within a run),
|
|
||||||
`/re/`, `?re?`, compound `+ - , ;` with sam defaults/precedence; commands
|
|
||||||
`a c i d`, `s` (with `sN`, `g`, `&`, `\1..\9`), `p =`, `m t`, `k`,
|
|
||||||
loops `x y g v` (nestable, `{}` groups), and shell filters `< > | !` via
|
|
||||||
`sh -c`. 27 unit tests cover each.
|
|
||||||
- **Multi-file `X`/`Y`** (`SamEngine::peelFileLoop` + Kate driver): a leading
|
|
||||||
`X/re/ cmd` runs `cmd` on every **open buffer** whose path matches `re` (`Y` =
|
|
||||||
non-match). The set is the documents Kate currently has open
|
|
||||||
(`Application::documents()`) — acme's open-window set — matched by local file
|
|
||||||
path, or display name for an unsaved scratch buffer. Nothing is opened or read
|
|
||||||
from disk; the live `Document`s are edited in place. sam permits only one
|
|
||||||
`X`/`Y` per command, so this is leading-token recognition, not a nested parse.
|
|
||||||
- **Undo**: single-document Run applies all edits inside one
|
|
||||||
`Document::EditingTransaction` → one Ctrl+Z reverts the whole Run. Multi-file
|
|
||||||
`X`/`Y` is undoable **per buffer** (KTextEditor has no global multi-file undo).
|
|
||||||
- **Panel** (`src/plugin/sampanel.{h,cpp}`): program editor + Run button +
|
|
||||||
output log; `OllieView` creates the tool view and owns the apply logic
|
|
||||||
(`runSamProgram`, `applySamToDocument`, `runSamFileLoop`).
|
|
||||||
- **Dialect & semantics**: the regex engine is sam's own (see the Regex engine
|
|
||||||
bullet above), so matching is leftmost-longest and the dialect is sam's — no
|
|
||||||
PCRE, no greedy-vs-longest divergence, no PCRE extras. Full user-facing
|
|
||||||
treatment with verified examples in **`docs/SAM.md`**. Out of scope: multi-file
|
|
||||||
menu (`b B n D`), external file I/O (`e r w f`), `"re"` file-addressing, and
|
|
||||||
sam's own `u` (Kate's undo stack is the undo mechanism). The live GUI panel
|
|
||||||
(tool view + transaction apply) is not exercisable headless and is unverified
|
|
||||||
on a live display.
|
|
||||||
|
|
||||||
## Build and test
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -B build -S .
|
|
||||||
cmake --build build
|
|
||||||
ctest --test-dir build
|
|
||||||
# or directly:
|
|
||||||
QT_QPA_PLATFORM=offscreen ./build/bin/test_fuzzyranker
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment facts (verified)
|
|
||||||
|
|
||||||
- Kate 25.12.3, KDE Frameworks 6, Qt 6.10, cmake 4.2.3, g++, extra-cmake-modules.
|
|
||||||
- KTextEditor + KCoreAddons + KConfigWidgets + KXmlGui + KRunner dev headers present.
|
|
||||||
- No Python/Pâté binding installed; C++ is the only first-class plugin path.
|
|
||||||
Loading…
Reference in New Issue