v1.1.1: theme-token diff card with old/new line-number gutter; hunks carry oldStart/newStart/rows
This commit is contained in:
1 parent
0dae0ee53d
commit
583aa9a10b
5 files changed
+135
-43
No files matched your search
@@ -94,7 +94,12 @@ Turn the guard off with `guard: false`, or change the list of blocked tools with
|
|||||||
|
|
||||||
## Diff cards in the Web UI
|
## Diff cards in the Web UI
|
||||||
|
|
||||||
The plugin ships a web client (`client.js`) that registers an `edit` tool view. Hashline, sloppy and apply_patch calls then render as DSH's native green/red diff card (from the applied hunks in `meta.diffs`), with `+N -M` counts, instead of raw patch text.
|
The plugin ships a web client (`client.js`) that registers an `edit` tool view. Hashline, sloppy and apply_patch calls render as a diff card instead of raw patch text:
|
||||||
|
- green added rows, red removed rows and gray context, using only DSH's theme tokens (`--dsw-alias-code-diff-added/-deleted`, `--dsw-alias-state-success/error-primary`, the code-block font), so it follows the active theme like the stock diff view;
|
||||||
|
- a **line-number gutter** with the old and new file line of every row;
|
||||||
|
- `+N -M` counts in the header, plus Wrap and Copy buttons.
|
||||||
|
|
||||||
|
The host attaches `oldStart`, `newStart` and `rows` to every hunk in `meta.diffs`, next to the standard `path`/`oldText`/`newText` fields. Older results without them still render, just without numbers.
|
||||||
|
|
||||||
## Edit modes
|
## Edit modes
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
// omp-edit Web client: render hashline / patch edits as DSH's native diff card.
|
// omp-edit Web client: render hashline / patch edits as a DSH-native diff card with line numbers.
|
||||||
//
|
//
|
||||||
// The stock `edit` row derives its diff from `file_path` + `old_string`/`new_string` arguments.
|
// The stock `edit` row derives its diff from `file_path` + `old_string`/`new_string` arguments.
|
||||||
// Hashline, sloppy and apply_patch calls carry one `input` string instead, so the stock row falls
|
// Hashline, sloppy and apply_patch calls carry one `input` string instead, so the stock row falls
|
||||||
// back to raw text. This keyed toolview (priority -1 shadows the stock one) renders the applied
|
// back to raw text. This keyed toolview (priority -1 shadows the stock one) renders the applied
|
||||||
// hunks the host already persists in `meta.diffs` with the shared DiffBlock primitive.
|
// hunks the host persists in `meta.diffs`.
|
||||||
|
//
|
||||||
|
// Styling uses only DSH theme tokens (the same ones DiffBlock.module.css uses: --dsw-alias-code-
|
||||||
|
// diff-added/-deleted, --dsw-alias-state-success-/error-primary, --dsw-font-markdown-code-block…)
|
||||||
|
// so it follows every theme. Each row shows its old/new file line numbers in a gutter, from the
|
||||||
|
// `oldStart`/`newStart`/`rows` the host attaches to every hunk (older results without them fall
|
||||||
|
// back to un-numbered rows).
|
||||||
window.__ModuleLoader__.load({
|
window.__ModuleLoader__.load({
|
||||||
id: '@local/omp-edit',
|
id: '@local/omp-edit',
|
||||||
factory(require) {
|
factory(require) {
|
||||||
@@ -12,15 +18,41 @@ window.__ModuleLoader__.load({
|
|||||||
const h = React.createElement;
|
const h = React.createElement;
|
||||||
|
|
||||||
const CSS = `
|
const CSS = `
|
||||||
.ompe-row{display:flex;flex-direction:column;gap:6px;min-width:0}
|
.ompe-row{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||||||
.ompe-head{display:flex;align-items:center;gap:6px;min-width:0;height:24px;padding:0 4px;border:0;background:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-radius:var(--dsw-radius-sm);text-align:left}
|
.ompe-head{display:flex;align-items:center;gap:6px;min-width:0;min-height:24px;padding:0 4px;margin:0 -4px;border:0;background:none;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;border-radius:var(--dsw-radius-sm);text-align:left}
|
||||||
.ompe-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
.ompe-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
||||||
|
.ompe-head:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary))}
|
||||||
|
.ompe-icon{display:inline-flex;flex:none;color:var(--dsw-alias-label-tertiary)}
|
||||||
.ompe-title{color:var(--dsw-alias-label-primary);flex:none}
|
.ompe-title{color:var(--dsw-alias-label-primary);flex:none}
|
||||||
.ompe-sum{color:var(--dsw-alias-label-caption);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
.ompe-sum{color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||||
.ompe-add{color:var(--dsw-alias-state-success,#3fb950);flex:none}
|
.ompe-add{color:var(--dsw-alias-state-success-primary);flex:none;font-variant-numeric:tabular-nums}
|
||||||
.ompe-del{color:var(--dsw-alias-state-danger,#f85149);flex:none}
|
.ompe-del{color:var(--dsw-alias-state-error-primary);flex:none;font-variant-numeric:tabular-nums}
|
||||||
.ompe-err{color:var(--dsw-alias-state-danger,#f85149)}
|
.ompe-err{color:var(--dsw-alias-state-error-primary)}
|
||||||
.ompe-text{margin:0;padding:8px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-l2,rgba(127,127,127,.08));white-space:pre-wrap;font:12px/1.5 var(--dsw-font-mono,ui-monospace,monospace);color:var(--dsw-alias-label-secondary);max-height:320px;overflow:auto}
|
.ompe-card{border-radius:var(--dsw-radius-lg,12px);background:var(--dsw-alias-bg-l2,var(--dsw-alias-fill-l1));border:1px solid var(--dsw-alias-border-l1,transparent);overflow:hidden}
|
||||||
|
.ompe-tools{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px 0 12px;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
||||||
|
.ompe-tools-path{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.ompe-tbtn{border:0;background:none;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;height:24px;padding:0 6px;border-radius:var(--dsw-radius-sm);cursor:pointer}
|
||||||
|
.ompe-tbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
|
||||||
|
.ompe-body{display:grid;grid-template-columns:auto auto minmax(max-content,1fr);padding:2px 0 8px;font:var(--dsw-font-markdown-code-block);overflow-x:auto;max-height:480px;overflow-y:auto}
|
||||||
|
.ompe-body[data-wrap]{grid-template-columns:auto auto minmax(0,1fr)}
|
||||||
|
.ompe-ln{padding:0 8px 0 10px;text-align:right;color:var(--dsw-alias-label-tertiary);opacity:.7;user-select:none;font-variant-numeric:tabular-nums;white-space:pre}
|
||||||
|
.ompe-ln+.ompe-ln{padding-left:0}
|
||||||
|
.ompe-code{padding:0 16px 0 6px;white-space:pre;min-height:1lh}
|
||||||
|
.ompe-body[data-wrap] .ompe-code{white-space:pre-wrap;overflow-wrap:anywhere}
|
||||||
|
.ompe-code::before{display:inline-block;width:1.5ch;content:' '}
|
||||||
|
.ompe-r-add>.ompe-code::before{content:'+'}
|
||||||
|
.ompe-r-del>.ompe-code::before{content:'-'}
|
||||||
|
.ompe-r{display:contents}
|
||||||
|
.ompe-r-ctx>*{color:var(--dsw-alias-label-secondary)}
|
||||||
|
.ompe-r-add>*{background:var(--dsw-alias-code-diff-added)}
|
||||||
|
.ompe-r-add>.ompe-code{color:var(--dsw-alias-state-success-primary)}
|
||||||
|
.ompe-r-add>.ompe-ln:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-success-primary)}
|
||||||
|
.ompe-r-del>*{background:var(--dsw-alias-code-diff-deleted)}
|
||||||
|
.ompe-r-del>.ompe-code{color:var(--dsw-alias-state-error-primary)}
|
||||||
|
.ompe-r-del>.ompe-ln:first-child{box-shadow:inset 3px 0 0 var(--dsw-alias-state-error-primary)}
|
||||||
|
.ompe-r-path>*{color:var(--dsw-alias-label-secondary);font-weight:500;padding-top:6px}
|
||||||
|
.ompe-r-gap>*{color:var(--dsw-alias-label-tertiary)}
|
||||||
|
.ompe-text{margin:0;padding:8px 12px;font:var(--dsw-font-markdown-code-block);white-space:pre-wrap;color:var(--dsw-alias-label-secondary);max-height:320px;overflow:auto}
|
||||||
`;
|
`;
|
||||||
let cssDone = false;
|
let cssDone = false;
|
||||||
const installCss = () => {
|
const installCss = () => {
|
||||||
@@ -33,26 +65,44 @@ window.__ModuleLoader__.load({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Icon = P.IconEditOutlineRegular || (() => null);
|
const Icon = P.IconEditOutlineRegular || (() => null);
|
||||||
const Chevron = P.IconChevronRightOutlineRegular || (() => null);
|
|
||||||
|
|
||||||
function readDiffs(meta) {
|
function readDiffs(meta) {
|
||||||
const d = meta && typeof meta === 'object' && Array.isArray(meta.diffs) ? meta.diffs : [];
|
const d = meta && typeof meta === 'object' && Array.isArray(meta.diffs) ? meta.diffs : [];
|
||||||
return d.filter((x) => x && typeof x.path === 'string' && typeof x.newText === 'string' && (x.oldText === null || typeof x.oldText === 'string'));
|
return d.filter((x) => x && typeof x.path === 'string' && typeof x.newText === 'string' && (x.oldText === null || typeof x.oldText === 'string'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function countLines(diffs) {
|
/** Hunk → numbered rows. Uses host-provided rows/starts; falls back to a naive old/new split. */
|
||||||
// Approximate +/- counts from hunk texts (context lines are shared by both sides).
|
function hunkRows(d) {
|
||||||
let add = 0; let del = 0;
|
const rows = Array.isArray(d.rows) ? d.rows : [
|
||||||
|
...(d.oldText === null ? [] : d.oldText.split('\n').map((t) => ['-', t])),
|
||||||
|
...d.newText.split('\n').map((t) => ['+', t]),
|
||||||
|
];
|
||||||
|
const numbered = Number.isInteger(d.oldStart) && Number.isInteger(d.newStart);
|
||||||
|
let o = d.oldStart; let n = d.newStart;
|
||||||
|
return rows.map(([op, text]) => {
|
||||||
|
const row = { kind: op === '+' ? 'add' : op === '-' ? 'del' : 'ctx', text, old: '', neu: '' };
|
||||||
|
if (numbered) {
|
||||||
|
if (op !== '+') row.old = String(o++);
|
||||||
|
if (op !== '-') row.neu = String(n++);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRows(diffs) {
|
||||||
|
const out = [];
|
||||||
|
let prev;
|
||||||
for (const d of diffs) {
|
for (const d of diffs) {
|
||||||
const a = d.oldText === null ? [] : d.oldText.split('\n');
|
if (d.path !== prev) out.push({ kind: 'path', text: d.path, old: '', neu: '' });
|
||||||
const b = d.newText.split('\n');
|
else out.push({ kind: 'gap', text: '⋯', old: '', neu: '' });
|
||||||
const common = new Set(a.filter((l) => b.includes(l)));
|
prev = d.path;
|
||||||
add += b.filter((l) => !common.has(l)).length;
|
out.push(...hunkRows(d));
|
||||||
del += a.filter((l) => !common.has(l)).length;
|
|
||||||
}
|
}
|
||||||
return { add, del };
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totals = (rows) => rows.reduce((a, r) => ({ add: a.add + (r.kind === 'add'), del: a.del + (r.kind === 'del') }), { add: 0, del: 0 });
|
||||||
|
|
||||||
function argsSummary(argsRaw) {
|
function argsSummary(argsRaw) {
|
||||||
try {
|
try {
|
||||||
const a = JSON.parse(argsRaw || '{}');
|
const a = JSON.parse(argsRaw || '{}');
|
||||||
@@ -66,11 +116,28 @@ window.__ModuleLoader__.load({
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const labels = (t) => ({
|
function copyText(rows) {
|
||||||
codeLabel: t('code') || 'Code', wrapLabel: t('wrap') || 'Wrap', unwrapLabel: t('unwrap') || 'No wrap',
|
return rows.map((r) => (r.kind === 'add' ? `+ ${r.text}` : r.kind === 'del' ? `- ${r.text}` : r.kind === 'ctx' ? ` ${r.text}` : r.text)).join('\n');
|
||||||
copy: t('copy') || 'Copy', copied: t('copied') || 'Copied',
|
}
|
||||||
collapseAria: 'Collapse', expandAria: (n) => `Show ${n} more lines`, collapse: t('collapse') || 'Collapse', expand: (n) => `Show ${n} more lines`,
|
|
||||||
});
|
function DiffCard({ rows, files, t }) {
|
||||||
|
const [wrap, setWrap] = React.useState(false);
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
const onCopy = () => {
|
||||||
|
navigator.clipboard?.writeText(copyText(rows)).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1000); }).catch(() => {});
|
||||||
|
};
|
||||||
|
const tr = (k, fb) => (typeof t === 'function' && t(k)) || fb;
|
||||||
|
return h('div', { className: 'ompe-card', 'data-diff': '' },
|
||||||
|
h('div', { className: 'ompe-tools' },
|
||||||
|
h('span', { className: 'ompe-tools-path' }, files),
|
||||||
|
h('button', { type: 'button', className: 'ompe-tbtn', onClick: () => setWrap((v) => !v) }, wrap ? tr('code.unwrap', 'No wrap') : tr('code.wrap', 'Wrap')),
|
||||||
|
h('button', { type: 'button', className: 'ompe-tbtn', onClick: onCopy }, copied ? tr('copied', 'Copied') : tr('copy', 'Copy'))),
|
||||||
|
h('div', { className: 'ompe-body', 'data-wrap': wrap ? '' : undefined },
|
||||||
|
rows.map((r, i) => h('div', { key: i, className: `ompe-r ompe-r-${r.kind}` },
|
||||||
|
h('span', { className: 'ompe-ln' }, r.old),
|
||||||
|
h('span', { className: 'ompe-ln' }, r.neu),
|
||||||
|
h('span', { className: 'ompe-code' }, r.text)))));
|
||||||
|
}
|
||||||
|
|
||||||
function EditRow(props) {
|
function EditRow(props) {
|
||||||
const { block, t, phase } = props;
|
const { block, t, phase } = props;
|
||||||
@@ -78,25 +145,25 @@ window.__ModuleLoader__.load({
|
|||||||
const [open, setOpen] = React.useState(true);
|
const [open, setOpen] = React.useState(true);
|
||||||
const done = block && 'kind' in block;
|
const done = block && 'kind' in block;
|
||||||
const diffs = done && !block.isError ? readDiffs(block.meta) : [];
|
const diffs = done && !block.isError ? readDiffs(block.meta) : [];
|
||||||
|
const rows = React.useMemo(() => buildRows(diffs), [block?.meta]);
|
||||||
const argsRaw = block?.call?.argsRaw ?? block?.argsRaw ?? '';
|
const argsRaw = block?.call?.argsRaw ?? block?.argsRaw ?? '';
|
||||||
const files = diffs.length ? [...new Set(diffs.map((d) => d.path))].join(', ') : argsSummary(argsRaw);
|
const files = diffs.length ? [...new Set(diffs.map((d) => d.path))].join(', ') : argsSummary(argsRaw);
|
||||||
const { add, del } = countLines(diffs);
|
const { add, del } = totals(rows);
|
||||||
const text = done && Array.isArray(block.content) ? block.content.filter((c) => c.type === 'text').map((c) => c.text).join('\n') : '';
|
const text = done && Array.isArray(block.content) ? block.content.filter((c) => c.type === 'text').map((c) => c.text).join('\n') : '';
|
||||||
const running = !done || phase === 'preparing';
|
const running = !done || phase === 'preparing';
|
||||||
const title = (typeof t === 'function' && t('tool.title.edit')) || 'Edit';
|
const title = (typeof t === 'function' && t('tool.title.edit')) || 'Edit';
|
||||||
return h('div', { className: 'ompe-row' },
|
return h('div', { className: 'ompe-row' },
|
||||||
h('button', { type: 'button', className: 'ompe-head', onClick: () => setOpen((v) => !v), 'aria-expanded': open },
|
h('button', { type: 'button', className: 'ompe-head', onClick: () => setOpen((v) => !v), 'aria-expanded': open },
|
||||||
h('span', { style: { transform: open ? 'rotate(90deg)' : 'none', transition: 'transform .12s', display: 'inline-flex' } }, h(Chevron, { size: 12 })),
|
h('span', { className: 'ompe-icon' }, h(Icon, { size: 14 })),
|
||||||
h(Icon, { size: 14 }),
|
|
||||||
h('span', { className: 'ompe-title' }, running ? `${title}…` : title),
|
h('span', { className: 'ompe-title' }, running ? `${title}…` : title),
|
||||||
files ? h('span', { className: 'ompe-sum' }, files) : null,
|
files ? h('span', { className: 'ompe-sum' }, files) : null,
|
||||||
diffs.length ? h('span', { className: 'ompe-add' }, `+${add}`) : null,
|
diffs.length ? h('span', { className: 'ompe-add' }, `+${add}`) : null,
|
||||||
diffs.length ? h('span', { className: 'ompe-del' }, `-${del}`) : null,
|
diffs.length ? h('span', { className: 'ompe-del' }, `-${del}`) : null,
|
||||||
done && block.isError ? h('span', { className: 'ompe-err' }, '✕') : null),
|
done && block.isError ? h('span', { className: 'ompe-err' }, '✕') : null),
|
||||||
open && diffs.length && P.DiffBlock
|
open && rows.length
|
||||||
? h(P.DiffBlock, { diffs, labels: labels(typeof t === 'function' ? t : () => ''), maxLines: 24 })
|
? h(DiffCard, { rows, files, t })
|
||||||
: open && done && text
|
: open && done && text
|
||||||
? h('pre', { className: `ompe-text${block.isError ? ' ompe-err' : ''}` }, text)
|
? h('div', { className: 'ompe-card' }, h('pre', { className: `ompe-text${block.isError ? ' ompe-err' : ''}` }, text))
|
||||||
: null);
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-11
@@ -1,4 +1,6 @@
|
|||||||
// Hunk diffs for the DSH diff card ({path, oldText, newText} per hunk, 3 lines of context).
|
// Hunk diffs for the DSH diff card ({path, oldText, newText} per hunk, 3 lines of context).
|
||||||
|
// Each hunk also carries `oldStart`/`newStart` (1-based file lines of its first row) and `rows`
|
||||||
|
// ([op, text] with op '=', '-', '+') so a client can draw a line-number gutter.
|
||||||
import { lineMap } from './store.js'
|
import { lineMap } from './store.js'
|
||||||
import { splitLines } from './text.js'
|
import { splitLines } from './text.js'
|
||||||
|
|
||||||
@@ -7,16 +9,16 @@ export function hunkDiffs(path, before, after, context = 3) {
|
|||||||
const b = splitLines(after).lines
|
const b = splitLines(after).lines
|
||||||
const map = lineMap(a, b)
|
const map = lineMap(a, b)
|
||||||
// Walk both sides, emitting ops: '=' common, '-' removed, '+' added.
|
// Walk both sides, emitting ops: '=' common, '-' removed, '+' added.
|
||||||
const ops = []
|
const ops = [] // [op, text, oldLine|null, newLine|null]
|
||||||
let i = 1; let j = 1
|
let i = 1; let j = 1
|
||||||
const pairs = [...map.entries()].sort((x, y) => x[0] - y[0])
|
const pairs = [...map.entries()].sort((x, y) => x[0] - y[0])
|
||||||
for (const [ai, bj] of pairs) {
|
for (const [ai, bj] of pairs) {
|
||||||
while (i < ai) ops.push(['-', a[i++ - 1]])
|
while (i < ai) { ops.push(['-', a[i - 1], i, null]); i++ }
|
||||||
while (j < bj) ops.push(['+', b[j++ - 1]])
|
while (j < bj) { ops.push(['+', b[j - 1], null, j]); j++ }
|
||||||
ops.push(['=', a[i - 1]]); i++; j++
|
ops.push(['=', a[i - 1], i, j]); i++; j++
|
||||||
}
|
}
|
||||||
while (i <= a.length) ops.push(['-', a[i++ - 1]])
|
while (i <= a.length) { ops.push(['-', a[i - 1], i, null]); i++ }
|
||||||
while (j <= b.length) ops.push(['+', b[j++ - 1]])
|
while (j <= b.length) { ops.push(['+', b[j - 1], null, j]); j++ }
|
||||||
const changed = ops.map((o, k) => (o[0] !== '=' ? k : -1)).filter((k) => k >= 0)
|
const changed = ops.map((o, k) => (o[0] !== '=' ? k : -1)).filter((k) => k >= 0)
|
||||||
if (!changed.length) return []
|
if (!changed.length) return []
|
||||||
const spans = []
|
const spans = []
|
||||||
@@ -27,12 +29,17 @@ export function hunkDiffs(path, before, after, context = 3) {
|
|||||||
else spans.push([s, e])
|
else spans.push([s, e])
|
||||||
}
|
}
|
||||||
return spans.map(([s, e]) => {
|
return spans.map(([s, e]) => {
|
||||||
const oldL = []; const newL = []
|
const oldL = []; const newL = []; const rows = []
|
||||||
|
let oldStart = null; let newStart = null
|
||||||
for (let k = s; k <= e; k++) {
|
for (let k = s; k <= e; k++) {
|
||||||
const [op, text] = ops[k]
|
const [op, text, ol, nl] = ops[k]
|
||||||
if (op !== '+') oldL.push(text)
|
if (op !== '+') { oldL.push(text); oldStart ??= ol }
|
||||||
if (op !== '-') newL.push(text)
|
if (op !== '-') { newL.push(text); newStart ??= nl }
|
||||||
|
rows.push([op, text])
|
||||||
}
|
}
|
||||||
return { path, oldText: oldL.length ? oldL.join('\n') : null, newText: newL.join('\n') }
|
// A pure insertion/deletion side has no line of its own: anchor it next to its neighbour.
|
||||||
|
oldStart ??= Math.max(1, (ops.slice(0, s).findLast((o) => o[2] !== null)?.[2] ?? 0) + 1)
|
||||||
|
newStart ??= Math.max(1, (ops.slice(0, s).findLast((o) => o[3] !== null)?.[3] ?? 0) + 1)
|
||||||
|
return { path, oldText: oldL.length ? oldL.join('\n') : null, newText: newL.join('\n'), oldStart, newStart, rows }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@local/omp-edit",
|
"name": "@local/omp-edit",
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"description": "oh-my-pi style editing for DSH: replaces the edit tool with hashline patches (content-hash tagged line anchors, block ops, cut/paste registers, multi-file, stale-tag recovery), plus replace/sloppy/apply_patch modes; tags read and grep output.",
|
"description": "oh-my-pi style editing for DSH: replaces the edit tool with hashline patches (content-hash tagged line anchors, block ops, cut/paste registers, multi-file, stale-tag recovery), plus replace/sloppy/apply_patch modes; tags read and grep output.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -194,3 +194,16 @@ test('grep pages at pageLines with an offset cursor', async () => {
|
|||||||
assert.doesNotMatch(p2.text, /offset=/)
|
assert.doesNotMatch(p2.text, /offset=/)
|
||||||
h.cleanup()
|
h.cleanup()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('edit result diffs carry file line numbers and rows for the gutter', async () => {
|
||||||
|
const { h, agent } = await setup()
|
||||||
|
h.write('long.txt', Array.from({ length: 30 }, (_, i) => `line ${i + 1}`).join('\n') + '\n')
|
||||||
|
const tag = /#([0-9A-F]{4})\]/.exec((await h.call(agent, 'read', { file_path: 'long.txt' })).text)[1]
|
||||||
|
const e = await h.call(agent, 'edit', { input: `[long.txt#${tag}]\nPUT 20.=20:\n+LINE TWENTY\n+extra` })
|
||||||
|
const [d] = e.meta.diffs
|
||||||
|
assert.equal(d.oldStart, 17)
|
||||||
|
assert.equal(d.newStart, 17)
|
||||||
|
assert.deepEqual(d.rows.slice(2, 6), [['=', 'line 19'], ['-', 'line 20'], ['+', 'LINE TWENTY'], ['+', 'extra']])
|
||||||
|
assert.equal(typeof d.oldText, 'string') // stock DiffHunk fields stay intact
|
||||||
|
h.cleanup()
|
||||||
|
})
|
||||||
Reference in new issue
Block a user