From 997055f0b54795a44085fda4d1f98275b573d3d5 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 8 Oct 2026 14:03:55 +0200 Subject: [PATCH] docs(sam): document PCRE deviations + implications for katesam usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docs/SAM.md — a user-facing reference for the regex deviation, with examples verified against the engine: - leftmost-greedy (PCRE) vs leftmost-longest (sam): diverges only on overlapping alternation (a|ab); order alternatives longest-first. - ^/$ anchor buffer-wide, not per-line, by default. Workarounds: structural ,x/.+/ … (preferred) or the (?m) inline flag. - identical behaviour table (. and newlines, empty-match advance, &/\N). - PCRE bonuses sam lacks: \d \w \b, lookahead, non-greedy, (?i)(?m)(?s). Cross-link from PLAN.md and surface the two biggest gotchas (buffer-wide anchors, alternation order) directly in the Sam panel hint label. --- docs/PLAN.md | 15 +++-- docs/SAM.md | 139 ++++++++++++++++++++++++++++++++++++++++ src/plugin/sampanel.cpp | 5 +- 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 docs/SAM.md diff --git a/docs/PLAN.md b/docs/PLAN.md index 03e78f7..5aaa55c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -384,11 +384,16 @@ program, click **Run** (or Ctrl+Return); each Run is one undo step. output log; `OllieView` creates the tool view and owns the apply logic (`runSamProgram`, `applySamToDocument`, `runSamFileLoop`). - **Deviations (documented)**: regexes use `QRegularExpression` (PCRE), not - plan9 `regexp(7)` — practical patterns match identically; `longest-leftmost` - edge cases differ. 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. + plan9 `regexp(7)`. Everyday patterns match identically; the one semantic + divergence is leftmost-greedy vs sam's leftmost-longest (bites only on + overlapping alternation like `a|ab`), and `^`/`$` anchor buffer-wide not + per-line by default. katesam gains PCRE extras (`\d \w \b`, lookahead, + non-greedy, inline flags). Full user-facing treatment with verified examples + and workarounds 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 diff --git a/docs/SAM.md b/docs/SAM.md new file mode 100644 index 0000000..41d65f6 --- /dev/null +++ b/docs/SAM.md @@ -0,0 +1,139 @@ +# katesam — sam structural-regexp editing in Kate + +The **Sam** tool view (left sidebar) runs the plan9 +[sam](https://doc.cat-v.org/plan_9/4th_edition/papers/sam/) command language +against the active document, or project-wide via `X`/`Y`. Type a program, press +**Ctrl+Return** or click **Run**. Each Run is one undo step (Ctrl+Z reverts the +whole Run; multi-file `X`/`Y` undoes per file). + +This document covers the **regex deviation** — katesam uses PCRE +(`QRegularExpression`), not plan9 `regexp(7)` — and what that means in practice. +Every example below is verified against the engine. + +## TL;DR + +- Everyday patterns behave **identically** to sam. +- One real semantic difference: **PCRE is leftmost-greedy, sam is + leftmost-longest.** It only bites on *overlapping alternation* (`a|ab`). +- `^` and `$` anchor to the **whole buffer, not each line**, by default. This is + the most common surprise. Use a structural loop or the `(?m)` flag. +- You *gain* PCRE features sam never had: `\d \w \s \b`, lookahead/lookbehind, + non-greedy `*?`, backreferences, inline flags `(?i)` `(?m)` `(?s)`. + +--- + +## The one semantic divergence: greedy vs longest + +sam's `regexp(7)` matches the **leftmost-longest** substring. PCRE matches +**leftmost**, then resolves alternation **left-to-right, first wins** (greedy +within a branch, but alternation order decides between branches). + +| program | input | sam (leftmost-longest) | **katesam (PCRE)** | +|---|---|---|---| +| `,s/a\|ab/X/` | `ab` | `X` (matches `ab`) | **`Xb`** (matches `a`) | +| `,s/ab\|a/X/` | `ab` | `X` | `X` | + +**Implication:** when alternatives overlap, **order them longest-first** +(`ab|a`, not `a|ab`). This is the only case where a working sam command can +silently produce a different edit in katesam. Non-overlapping alternation +(`cat|dog`) and all non-alternation patterns are unaffected. + +Quantifier greediness is the same in both (`a.*c` on `axxcxxc` → whole string in +both). katesam additionally offers non-greedy `.*?`: + +| program | input | result | +|---|---|---| +| `,s/a.*c/X/` | `axxcxxc` | `X` (greedy, same as sam) | +| `,s/a.*?c/X/` | `axxcxxc` | `Xxxc` (non-greedy, PCRE bonus) | + +--- + +## Anchors `^` / `$` are buffer-wide, not per-line + +By default PCRE anchors `^`/`$` to the **start/end of the whole buffer**, so a +global substitution touches only the first/last position — **not** every line: + +| program | input | result | +|---|---|---| +| `,s/^/> /g` | `a⏎b⏎c` | `> a⏎b⏎c` (only line 1!) | +| `,s/$/;/g` | `a⏎b⏎c` | `a⏎b⏎c;` (only the end!) | + +This differs from how people expect line-oriented edits to work. **Two correct +ways** to act per line: + +**1. Structural (sam-idiomatic, preferred).** Loop over lines with `x`, then act +on each line's dot. `.+` matches the non-newline run of each line: + +``` +,x/.+/ i/> / prefix every (non-empty) line with "> " → > a⏎> b⏎> c +,x/.+/ a/;/ append ";" to every line → a;⏎b;⏎c; +``` + +**2. PCRE multiline flag `(?m)`** (a bonus sam lacks). Makes `^`/`$` match at +every line boundary: + +``` +,s/(?m)^/> /g → > a⏎> b⏎> c +,s/(?m)$/;/g → a;⏎b;⏎c; +``` + +Prefer the structural form — it is the real sam way and composes with other +loop/guard steps. Reach for `(?m)` for quick one-liners. + +--- + +## What matches identically (no surprises) + +| behaviour | katesam (PCRE) | sam | same? | +|---|---|---|---| +| `.` crosses newline | no | no | ✅ | +| `[^z]+` crosses newline | yes | yes | ✅ | +| empty-match advance `s/x*/-/g` on `axbx` → `-a-b-` | ✅ | ✅ | ✅ | +| greedy quantifiers `* + ?` | ✅ | ✅ | ✅ | +| character classes `[a-z]`, groups `( )`, `|` | ✅ | ✅ | ✅ | +| `&` whole-match, `\1..\9` groups in replacement | ✅ | ✅ | ✅ | + +> `.` not crossing newlines matches sam, so line-structured descent +> (`,x/.*\n/ ...`) behaves as expected. Use `(?s)` if you *want* `.` to span +> newlines (PCRE bonus). + +--- + +## PCRE features sam never had (free upgrades) + +All verified working in katesam: + +| feature | example | effect | +|---|---|---| +| shorthand classes | `,s/\d+/N/g` on `a12b345` | `aNbN` | +| `\w+` in a loop | `,x/\w+/ c/W/` on `foo bar` | `W W` | +| word boundary `\b` | `,s/\bfoo\b/X/g` on `foo foobar foo` | `X foobar X` | +| backreference | `,s/(\w)\1/D/g` on `aabbc` | `DDc` | +| lookahead | `,s/foo(?=bar)/X/g` on `foobar fooqux` | `Xbar fooqux` | +| inline flags | `(?i)` ignore-case, `(?m)` multiline, `(?s)` dotall | per-pattern | + +These make many edits *easier* than in real sam. The tradeoff is that a pattern +written for katesam may not be portable back to sam. + +--- + +## Practical guidance + +- **Order overlapping alternatives longest-first** (`\.tar\.gz|\.gz`, not the + reverse). The only silent divergence. +- **For per-line edits, prefer `,x/.+/ …`** over `^`/`$`. If you use anchors + globally, remember they are buffer-wide, or add `(?m)`. +- **Lean on PCRE extras** (`\b`, `\d`, lookahead) freely — they are reliable; + just know they are not sam-portable. +- **Empty-match globals** (`s/x*/…/g`) advance one position per null match, as in + sam — safe, but as always with `*`, double-check the result. + +## Why PCRE and not regexp(7) + +`QRegularExpression` ships with Qt (already a dependency) and gives a richer, +familiar syntax for free. plan9 `regexp(7)` is a Rune-based NFA woven into sam's +own buffer types; using it would mean porting that engine. The tradeoff accepted +here: everyday fidelity plus extra power, at the cost of exact leftmost-longest +semantics on overlapping alternation. If that matters for your workflow, the +engine's regex calls are isolated in `SamEngine` and could be swapped for a +ported `regexp.c` later. diff --git a/src/plugin/sampanel.cpp b/src/plugin/sampanel.cpp index 1bb60cc..354875b 100644 --- a/src/plugin/sampanel.cpp +++ b/src/plugin/sampanel.cpp @@ -55,7 +55,10 @@ SamPanel::SamPanel(QWidget *parent) auto *hint = new QLabel( i18n("sam commands — e.g. ,s/foo/bar/g or " - "X/\\.cpp$/ ,s/old/new/g (project-wide). Ctrl+Return runs."), + "X/\\.cpp$/ ,s/old/new/g (project-wide). Ctrl+Return runs.
" + "Regex is PCRE: ^/$ are buffer-wide (use " + ",x/.+/ … or (?m) per line); order overlapping " + "alternatives longest-first."), this); hint->setWordWrap(true); hint->setTextFormat(Qt::RichText);