dsh-diff-approval 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +930 -95
- package/lib/index.js +39 -8
- package/lib/types/client/PendingPanel.d.ts +42 -0
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/settings.d.ts +9 -0
- package/lib/types/client/split-diff.d.ts +51 -0
- package/lib/types/client/whole-file-diff.d.ts +4 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -694,6 +694,35 @@ const inject = [
|
|
|
694
694
|
];
|
|
695
695
|
/** The connection RPC channel this plugin serves. */
|
|
696
696
|
const DIFF_APPROVAL_CHANNEL = "/diff-approval";
|
|
697
|
+
/** The dominant line ending of `text`: CRLF when `\r\n` meets or beats lone
|
|
698
|
+
* `\n`, else LF; a text with no line breaks falls back to LF.
|
|
699
|
+
* @param text - the content to inspect.
|
|
700
|
+
* @returns the dominant line-ending sequence.
|
|
701
|
+
*/
|
|
702
|
+
function detectEol(text) {
|
|
703
|
+
let crlf = 0;
|
|
704
|
+
let lf = 0;
|
|
705
|
+
for (let i = 0; i < text.length; i++) {
|
|
706
|
+
const c = text.charCodeAt(i);
|
|
707
|
+
if (c === 13) {
|
|
708
|
+
if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) {
|
|
709
|
+
crlf++;
|
|
710
|
+
i++;
|
|
711
|
+
}
|
|
712
|
+
} else if (c === 10) lf++;
|
|
713
|
+
}
|
|
714
|
+
if (crlf === 0 && lf === 0) return "\n";
|
|
715
|
+
return crlf >= lf ? "\r\n" : "\n";
|
|
716
|
+
}
|
|
717
|
+
/** Normalize any line ending (`\r\n`, `\r`) to `\n`. */
|
|
718
|
+
function normalizeEol(text) {
|
|
719
|
+
return text.replace(/\r\n?/g, "\n");
|
|
720
|
+
}
|
|
721
|
+
/** Re-encode `text`'s line endings to `eol` (its content is unchanged). */
|
|
722
|
+
function reencodeEol(text, eol) {
|
|
723
|
+
const normalized = normalizeEol(text);
|
|
724
|
+
return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
|
|
725
|
+
}
|
|
697
726
|
/**
|
|
698
727
|
* Narrow a successful `edit` result value to an operation outcome. The edit
|
|
699
728
|
* tool's output schema declares exactly `{ path, before, after }`; anything
|
|
@@ -1275,7 +1304,8 @@ function apply(ctx, config) {
|
|
|
1275
1304
|
if (entry.kind === "create") await rm(ctx.fs.processPath(resolved), { force: true });
|
|
1276
1305
|
else {
|
|
1277
1306
|
const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
|
|
1278
|
-
|
|
1307
|
+
const content = reencodeEol(entry.oldText, detectEol(entry.newText));
|
|
1308
|
+
await writeRevert(resolved, content, target.sessionId, signal);
|
|
1279
1309
|
undo = {
|
|
1280
1310
|
before: {
|
|
1281
1311
|
id: entry.id,
|
|
@@ -1287,7 +1317,7 @@ function apply(ctx, config) {
|
|
|
1287
1317
|
id: entry.id,
|
|
1288
1318
|
path: entry.path,
|
|
1289
1319
|
entry: void 0,
|
|
1290
|
-
fileText:
|
|
1320
|
+
fileText: content
|
|
1291
1321
|
}
|
|
1292
1322
|
};
|
|
1293
1323
|
}
|
|
@@ -1353,25 +1383,26 @@ function apply(ctx, config) {
|
|
|
1353
1383
|
};
|
|
1354
1384
|
const restored = contentRangeOf(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd);
|
|
1355
1385
|
const updatedNew = replaceContentLines(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd, restored);
|
|
1386
|
+
const content = reencodeEol(updatedNew, detectEol(entry.newText));
|
|
1356
1387
|
let afterEntry;
|
|
1357
|
-
if (updatedNew === entry.oldText) {
|
|
1388
|
+
if (normalizeEol(updatedNew) === normalizeEol(entry.oldText)) {
|
|
1358
1389
|
store.remove(blockTarget.sessionId, blockTarget.id);
|
|
1359
1390
|
afterEntry = void 0;
|
|
1360
1391
|
} else {
|
|
1361
|
-
store.update(blockTarget.sessionId, blockTarget.id, { newText:
|
|
1392
|
+
store.update(blockTarget.sessionId, blockTarget.id, { newText: content });
|
|
1362
1393
|
afterEntry = {
|
|
1363
1394
|
...entry,
|
|
1364
|
-
newText:
|
|
1395
|
+
newText: content,
|
|
1365
1396
|
updatedAt: Date.now()
|
|
1366
1397
|
};
|
|
1367
1398
|
}
|
|
1368
1399
|
let undo;
|
|
1369
1400
|
try {
|
|
1370
1401
|
const resolved = await ctx.fs.resolve(entry.path, { signal });
|
|
1371
|
-
if (entry.kind === "create" &&
|
|
1402
|
+
if (entry.kind === "create" && content === "") await rm(ctx.fs.processPath(resolved), { force: true });
|
|
1372
1403
|
else {
|
|
1373
1404
|
const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
|
|
1374
|
-
await writeRevert(resolved,
|
|
1405
|
+
await writeRevert(resolved, content, blockTarget.sessionId, signal);
|
|
1375
1406
|
undo = {
|
|
1376
1407
|
before: {
|
|
1377
1408
|
id: entry.id,
|
|
@@ -1383,7 +1414,7 @@ function apply(ctx, config) {
|
|
|
1383
1414
|
id: entry.id,
|
|
1384
1415
|
path: entry.path,
|
|
1385
1416
|
entry: afterEntry,
|
|
1386
|
-
fileText:
|
|
1417
|
+
fileText: content
|
|
1387
1418
|
}
|
|
1388
1419
|
};
|
|
1389
1420
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/** Sidebar-foot pending-edit review action and the split review panel it opens. */
|
|
2
|
+
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
|
|
2
3
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
|
+
import type { DiffApprovalBlockRange, PendingFileDiff } from '../types.ts';
|
|
3
5
|
import type { PendingPanelFace } from './slots.ts';
|
|
6
|
+
import type { DiffApprovalKey } from './locales.ts';
|
|
7
|
+
import { computeWholeFileDiff } from './whole-file-diff.ts';
|
|
8
|
+
import { highlightLines } from './highlight.ts';
|
|
4
9
|
export declare function wrapInto(text: string, widthPx: number, measure: (t: string) => number, tabPx: number): string[];
|
|
5
10
|
/** Full panel props composed by the sidebar footer-action slot. */
|
|
6
11
|
export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
|
|
12
|
+
/** Locale translator used by the panel and its rows. */
|
|
13
|
+
type Translator = (key: DiffApprovalKey, params?: Record<string, unknown>) => string;
|
|
7
14
|
/**
|
|
8
15
|
* Open the DSH settings dialog and switch to this plugin's section. The
|
|
9
16
|
* settings shell keeps its open state and the active section id as
|
|
@@ -14,5 +21,40 @@ export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFa
|
|
|
14
21
|
* @param sectionLabel - the nav label of this plugin's settings section.
|
|
15
22
|
*/
|
|
16
23
|
export declare function openSettingsSection(sectionLabel: string): void;
|
|
24
|
+
/** One file's synchronous view: diff rows and change blocks. */
|
|
25
|
+
interface RowModel {
|
|
26
|
+
diff: ReturnType<typeof computeWholeFileDiff>;
|
|
27
|
+
/** Maximal runs of changed rows (inclusive row indices); one per modification. */
|
|
28
|
+
blocks: ChangeBlock[];
|
|
29
|
+
}
|
|
30
|
+
/** One file's deferred syntax-highlight runs, one entry per side. */
|
|
31
|
+
interface HighlightRuns {
|
|
32
|
+
oldRuns: ReturnType<typeof highlightLines>;
|
|
33
|
+
newRuns: ReturnType<typeof highlightLines>;
|
|
34
|
+
}
|
|
35
|
+
/** One contiguous run of changed rows, treated as a single modification. */
|
|
36
|
+
interface ChangeBlock {
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
}
|
|
40
|
+
/** Imperative surface the parent uses to drive block navigation from the
|
|
41
|
+
* shared toolbar/keyboard in split mode (its own `focus` is private here). */
|
|
42
|
+
export interface SplitDiffHandle {
|
|
43
|
+
jump: (direction: -1 | 1) => void;
|
|
44
|
+
openSearch: () => void;
|
|
45
|
+
}
|
|
46
|
+
/** The two-column (side-by-side) whole-file diff view. */
|
|
47
|
+
export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
|
|
48
|
+
file: PendingFileDiff;
|
|
49
|
+
model: RowModel;
|
|
50
|
+
runs: HighlightRuns | undefined;
|
|
51
|
+
langWrap: boolean;
|
|
52
|
+
tabWidthSpaces: number;
|
|
53
|
+
busy: boolean;
|
|
54
|
+
t: Translator;
|
|
55
|
+
onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
|
|
56
|
+
onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
|
|
57
|
+
} & import("react").RefAttributes<SplitDiffHandle>>;
|
|
17
58
|
/** Render the pending-edit review panel and its unified footer action. */
|
|
18
59
|
export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, onAckRedoCleared, t, }: PendingPanelProps): import("react").JSX.Element;
|
|
60
|
+
export {};
|
|
@@ -24,6 +24,8 @@ export declare const zh: {
|
|
|
24
24
|
'panel.importUntrackedDesc': string;
|
|
25
25
|
'panel.tabWidth': string;
|
|
26
26
|
'panel.tabWidthDesc': string;
|
|
27
|
+
'panel.splitMode': string;
|
|
28
|
+
'panel.splitModeDesc': string;
|
|
27
29
|
'settings.tabLabel': string;
|
|
28
30
|
'row.create': string;
|
|
29
31
|
'row.failed': string;
|
|
@@ -95,6 +97,8 @@ export declare const en: {
|
|
|
95
97
|
'panel.importUntrackedDesc': string;
|
|
96
98
|
'panel.tabWidth': string;
|
|
97
99
|
'panel.tabWidthDesc': string;
|
|
100
|
+
'panel.splitMode': string;
|
|
101
|
+
'panel.splitModeDesc': string;
|
|
98
102
|
'settings.tabLabel': string;
|
|
99
103
|
'row.create': string;
|
|
100
104
|
'row.failed': string;
|
|
@@ -36,3 +36,12 @@ export declare function setWrapEnabled(lang: string, value: boolean): void;
|
|
|
36
36
|
export declare function tabWidth(): number;
|
|
37
37
|
/** Persist the diff's tab width (in spaces). */
|
|
38
38
|
export declare function setTabWidth(value: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the whole-file diff view uses the two-column (side-by-side) layout.
|
|
41
|
+
* Default off (single column): the unified diff. Only an explicit `'1'` enables
|
|
42
|
+
* split mode.
|
|
43
|
+
* @returns whether the split (two-column) diff view is used.
|
|
44
|
+
*/
|
|
45
|
+
export declare function splitMode(): boolean;
|
|
46
|
+
/** Persist the split-view preference. */
|
|
47
|
+
export declare function setSplitMode(value: boolean): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-by-side (split) diff model: the unified whole-file diff rows are
|
|
3
|
+
* regrouped into line-aligned pairs for a two-column "before | current" view.
|
|
4
|
+
* Pure derivation; the view owns rendering. A context row becomes a pair with
|
|
5
|
+
* both sides, a deletion a left-only pair, an addition a right-only pair, and
|
|
6
|
+
* an adjacent deletion/addition run is paired line-by-line into a single
|
|
7
|
+
* replacement pair so the two columns line up.
|
|
8
|
+
* @module dsh-diff-approval/client/split-diff
|
|
9
|
+
*/
|
|
10
|
+
import type { WholeFileDiffRow } from './whole-file-diff.ts';
|
|
11
|
+
/** One side of a split pair: text plus its 1-based line number (absent on a pure add). */
|
|
12
|
+
export interface SplitSide {
|
|
13
|
+
/** The side's source line content, without the terminating newline. */
|
|
14
|
+
text: string;
|
|
15
|
+
/** 1-based line number on this side, or undefined. */
|
|
16
|
+
line: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** One aligned left/right pair of the split view. */
|
|
19
|
+
export interface SplitPair {
|
|
20
|
+
/** What the pair is: unchanged, deleted (left only), added (right only), or a replaced line. */
|
|
21
|
+
kind: 'context' | 'del' | 'add' | 'replace';
|
|
22
|
+
/** The old (left) side; absent on a pure addition. */
|
|
23
|
+
left: SplitSide | undefined;
|
|
24
|
+
/** The new (right) side; absent on a pure deletion. */
|
|
25
|
+
right: SplitSide | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** The derived split view: aligned pairs plus the row→pair index map. */
|
|
28
|
+
export interface SplitDiff {
|
|
29
|
+
/** Aligned pairs in file order. */
|
|
30
|
+
pairs: SplitPair[];
|
|
31
|
+
/** Original whole-file row index → pair index, so block ranges keep working. */
|
|
32
|
+
pairOfRow: ReadonlyMap<number, number>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Regroup the whole-file rows into aligned split pairs, pairing a deletion run
|
|
36
|
+
* with a following addition run line-by-line (so a replaced line is one pair),
|
|
37
|
+
* and leaving a stray deletion or addition as a one-sided pair.
|
|
38
|
+
* @param rows - the whole-file diff rows.
|
|
39
|
+
* @returns the split pairs plus the row→pair index map.
|
|
40
|
+
*/
|
|
41
|
+
export declare function computeSideBySideDiff(rows: readonly WholeFileDiffRow[]): SplitDiff;
|
|
42
|
+
/**
|
|
43
|
+
* Pair indices whose left or right text contains the query (case-insensitive).
|
|
44
|
+
* A pair counts once however many times the query appears, so split search
|
|
45
|
+
* highlights the whole pair on both columns and a single "current" pair is
|
|
46
|
+
* stepped through — not each individual left/right occurrence.
|
|
47
|
+
* @param pairs - the split pairs.
|
|
48
|
+
* @param query - the query text; an empty query matches nothing.
|
|
49
|
+
* @returns matching pair indices, in file order.
|
|
50
|
+
*/
|
|
51
|
+
export declare function searchPairs(pairs: readonly SplitPair[], query: string): number[];
|
|
@@ -29,7 +29,10 @@ export interface WholeFileDiff {
|
|
|
29
29
|
* Compute the whole-file view between two contents. The context budget is the
|
|
30
30
|
* side lengths, so every hunk covers the file and unchanged lines survive as
|
|
31
31
|
* context rows; the `` patch marker is annotation
|
|
32
|
-
* and never becomes a row.
|
|
32
|
+
* and never becomes a row. Both sides are line-ending normalized (`\r\n?` →
|
|
33
|
+
* `\n`) first, so a repo baseline stored with one EOL and a worktree with
|
|
34
|
+
* another never show as a whole-file delete+add — the diff is about approved
|
|
35
|
+
* content, not line-ending noise.
|
|
33
36
|
* @param oldText - the file content before the pending change.
|
|
34
37
|
* @param newText - the file content after the pending change.
|
|
35
38
|
* @returns the complete row list with totals.
|
package/package.json
CHANGED