reslop 0.2.6 → 0.3.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.
Files changed (158) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/dashboard/blocks.js +490 -0
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +19 -61
  72. package/lib/render/tiles.js +203 -47
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +166 -59
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +72 -59
  126. package/lib/session/workspace.js +416 -0
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -1,13 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const metautil = require('metautil');
4
- const { jsonParse } = metautil;
5
- const files = require('./files.js');
6
- const { itemPath } = files;
7
- const patch = require('./diff/patch.js');
3
+ const { jsonParse } = require('metautil');
4
+ const { itemPath } = require('../common/files.js');
5
+ const patch = require('../diff/patch.js');
8
6
  const { countSides, formatHunkHeader, formatPatch } = patch;
9
- const diff = require('./diff/diff.js');
10
- const { isCtxType } = diff;
7
+ const { isCtxType } = require('../diff/diff.js');
11
8
  const manifest = require('./manifest.js');
12
9
  const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
13
10
  const { listFilePath, readSections, emptySectionMaps } = manifest;
@@ -66,8 +63,10 @@ const lineEntryName = (text) => {
66
63
  return entry ? entry[1] : '';
67
64
  };
68
65
 
69
- const lineInBlock = (item, line) =>
70
- typeof item.blockId !== 'number' || line.blockId === item.blockId;
66
+ const lineInBlock = (item, line) => {
67
+ const open = typeof item.blockId !== 'number';
68
+ return open || line.blockId === item.blockId;
69
+ };
71
70
 
72
71
  const hunkOnlyFoldedDeps = (item, names) => {
73
72
  if (!item.hunk || !names.size) return false;
@@ -86,8 +85,10 @@ const hunkOnlyFoldedDeps = (item, names) => {
86
85
 
87
86
  const stripFoldedDepItem = (item, names) => {
88
87
  if (!item.hunk || !names.size) return item;
89
- const isFolded = (line) =>
90
- line.type !== 'ctx' && names.has(lineEntryName(line.text));
88
+ const isFolded = (line) => {
89
+ const changed = line.type !== 'ctx';
90
+ return changed && names.has(lineEntryName(line.text));
91
+ };
91
92
  const lines = item.hunk.lines.filter((line) => !isFolded(line));
92
93
  if (lines.length === item.hunk.lines.length) return item;
93
94
  const isEdit = (line) => line.type !== 'ctx' && lineInBlock(item, line);
@@ -164,8 +165,15 @@ const mergeResolved = (changes, oldResolved, newResolved, oldMaps, newMaps) => {
164
165
  const action = changeAction(from, to);
165
166
  const resolvedFrom = from;
166
167
  const resolvedTo = to;
167
- const section = 'resolved';
168
- merged.push({ name, section, action, from, to, resolvedFrom, resolvedTo });
168
+ merged.push({
169
+ name,
170
+ section: 'resolved',
171
+ action,
172
+ from,
173
+ to,
174
+ resolvedFrom,
175
+ resolvedTo,
176
+ });
169
177
  }
170
178
  return merged;
171
179
  };
@@ -194,29 +202,31 @@ const readJsonSides = (readSides, origin, rel) => {
194
202
 
195
203
  const sectionMapsOf = (pkg, lock, manifestItems) => {
196
204
  if (pkg.before || pkg.after) {
197
- return {
198
- oldMaps: readSections(pkg.before),
199
- newMaps: readSections(pkg.after),
200
- };
205
+ const oldMaps = readSections(pkg.before);
206
+ const newMaps = readSections(pkg.after);
207
+ return { oldMaps, newMaps };
201
208
  }
202
209
  if (manifestItems.length) return mapsFromHunks(manifestItems);
203
210
  const oldMaps = lockRootSections(lock.before);
204
- return { oldMaps, newMaps: lockRootSections(lock.after) };
211
+ const newMaps = lockRootSections(lock.after);
212
+ return { oldMaps, newMaps };
205
213
  };
206
214
 
207
215
  const manifestPolicy = (manifestItems, pkg, hasChanges) => {
208
216
  if (!manifestItems.length) {
209
- return { foldManifest: false, takeAllManifest: false };
217
+ const foldManifest = false;
218
+ const takeAllManifest = false;
219
+ return { foldManifest, takeAllManifest };
210
220
  }
211
221
  if (pkg.before || pkg.after) {
212
222
  const onlyDeps = !hasFieldChanges(pkg.before, pkg.after);
213
- return {
214
- foldManifest: hasChanges,
215
- takeAllManifest: hasChanges && onlyDeps,
216
- };
223
+ const foldManifest = hasChanges;
224
+ const takeAllManifest = hasChanges && onlyDeps;
225
+ return { foldManifest, takeAllManifest };
217
226
  }
218
- const fold = hasChanges && manifestItems.every(isDepOnlyItem);
219
- return { foldManifest: fold, takeAllManifest: fold };
227
+ const foldManifest = hasChanges && manifestItems.every(isDepOnlyItem);
228
+ const takeAllManifest = foldManifest;
229
+ return { foldManifest, takeAllManifest };
220
230
  };
221
231
 
222
232
  const summarizeGroup = (group, readSides) => {
@@ -1,14 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const path = require('node:path');
4
- const metautil = require('metautil');
5
- const { isHashObject, jsonParse } = metautil;
6
- const utilities = require('./utilities.js');
7
- const { parseVersion, cmpVersion } = utilities;
8
-
9
- const MANIFEST = 'package.json';
10
- const LOCKFILE = 'package-lock.json';
11
- const DEP_KIND = { [MANIFEST]: 'manifest', [LOCKFILE]: 'lockfile' };
3
+ const { isHashObject, jsonParse } = require('metautil');
4
+ const { parseVersion, cmpVersion } = require('../common/utilities.js');
5
+ const files = require('../common/files.js');
6
+ const { MANIFEST, LOCKFILE, depFileMeta, relOf, listFilePath } = files;
7
+
12
8
  const SECTION_KEYS = [
13
9
  'dependencies',
14
10
  'devDependencies',
@@ -19,23 +15,6 @@ const LOCK_PREFIX = 'node_modules/';
19
15
 
20
16
  const isObject = (value) => !!value && typeof value === 'object';
21
17
 
22
- const posixRel = (rel) => `${rel ?? ''}`.replaceAll('\\', '/');
23
-
24
- const depFileMeta = (rel) => {
25
- const normalized = posixRel(rel);
26
- const kind = DEP_KIND[path.posix.basename(normalized)];
27
- if (!normalized || !kind) return null;
28
- return { kind, dir: path.posix.dirname(normalized) };
29
- };
30
-
31
- const relOf = (dir, name) => (dir === '.' ? name : `${dir}/${name}`);
32
-
33
- const listFilePath = (rel) => {
34
- const meta = depFileMeta(rel);
35
- if (meta && meta.kind === 'lockfile') return relOf(meta.dir, MANIFEST);
36
- return posixRel(rel);
37
- };
38
-
39
18
  const readSectionMap = (pkg, key) => {
40
19
  const map = new Map();
41
20
  const raw = pkg ? pkg[key] : null;
@@ -155,14 +134,17 @@ const seedManifest = (oldObj, newObj) => {
155
134
  return base;
156
135
  };
157
136
 
137
+ const mergedBase = (parsedBase, oldObj, newObj, kind) => {
138
+ if (parsedBase) return cloneJson(parsedBase);
139
+ if (kind === 'manifest') return seedManifest(oldObj, newObj);
140
+ return cloneJson(oldObj) || {};
141
+ };
142
+
158
143
  const mergeDepFile = (baseText, oldText, newText, change, side, kind) => {
159
144
  const oldObj = jsonParse(oldText ?? '');
160
145
  const newObj = jsonParse(newText ?? '');
161
146
  const parsedBase = jsonParse(baseText ?? '');
162
- let base;
163
- if (parsedBase) base = cloneJson(parsedBase);
164
- else if (kind === 'manifest') base = seedManifest(oldObj, newObj);
165
- else base = cloneJson(oldObj) || {};
147
+ const base = mergedBase(parsedBase, oldObj, newObj, kind);
166
148
  if (kind === 'lockfile') applyLockChange(base, change, side, oldObj, newObj);
167
149
  else applyManifestChange(base, change, side);
168
150
  return stringifyJson(base, baseText || newText || oldText || '');
@@ -247,7 +229,42 @@ const proposedWanted = (out, vul) => {
247
229
  return '';
248
230
  };
249
231
 
232
+ const cmpText = (left, right) => {
233
+ const a = parseVersion(left);
234
+ const b = parseVersion(right);
235
+ if (!a || !b) return null;
236
+ return cmpVersion(a, b);
237
+ };
238
+
239
+ const boundHolds = (cmp, op) => {
240
+ if (op === '<') return cmp < 0;
241
+ if (op === '<=') return cmp <= 0;
242
+ if (op === '>') return cmp > 0;
243
+ return cmp >= 0;
244
+ };
245
+
246
+ const inClause = (version, clause) => {
247
+ const ver = parseVersion(version);
248
+ if (!ver) return false;
249
+ const bounds = [...clause.matchAll(/(>=|<=|>|<)\s*(\d+\.\d+\.\d+)/g)];
250
+ if (!bounds.length) return false;
251
+ for (const match of bounds) {
252
+ const bound = parseVersion(match[2]);
253
+ if (!bound) return false;
254
+ if (!boundHolds(cmpVersion(ver, bound), match[1])) return false;
255
+ }
256
+ return true;
257
+ };
258
+
259
+ const inRange = (version, range) =>
260
+ `${range ?? ''}`.split('||').some((clause) => inClause(version, clause));
261
+
262
+ const hasRange = (range) => /[<>]=?\s*\d+\.\d+\.\d+/.test(`${range ?? ''}`);
263
+
250
264
  module.exports = {
265
+ cmpText,
266
+ inRange,
267
+ hasRange,
251
268
  MANIFEST,
252
269
  LOCKFILE,
253
270
  SECTION_KEYS,
package/lib/diff/align.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const inline = require('./inline.js');
4
- const { diffChars, tokenSimilarity } = inline;
3
+ const { diffChars, tokenSimilarity } = require('./inline.js');
5
4
 
6
5
  const PAIR_MIN_SIM = 0.3;
7
6
  const PAIR_HEAD_WEIGHT = 0.4;
package/lib/diff/diff.js CHANGED
@@ -5,7 +5,6 @@ const align = require('./align.js');
5
5
  const parse = require('./parse.js');
6
6
  const patch = require('./patch.js');
7
7
  const display = require('./display.js');
8
- const edit = require('./edit.js');
9
8
  const items = require('./items.js');
10
9
  const unit = require('./unit.js');
11
10
 
@@ -19,7 +18,8 @@ module.exports = {
19
18
  flattenBlock: patch.flattenBlock,
20
19
  formatPatch: patch.formatPatch,
21
20
  ...display,
22
- ...edit,
21
+ addBlockRange: patch.addBlockRange,
22
+ replaceBlockAdds: patch.replaceBlockAdds,
23
23
  ...items,
24
24
  ...unit,
25
25
  };
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const align = require('./align.js');
4
- const { isCtxType, attachInline, LAYOUT_ALIGN } = align;
5
- const patch = require('./patch.js');
6
- const { flattenBlock } = patch;
3
+ const { isCtxType, attachInline, LAYOUT_ALIGN } = require('./align.js');
4
+ const { flattenBlock } = require('./patch.js');
7
5
 
8
6
  const DISPLAY_CONTEXT = 3;
9
7
 
@@ -80,11 +78,37 @@ const blockAddText = (hunk, blockId) => {
80
78
  return parts.join('\n');
81
79
  };
82
80
 
81
+ const countsOld = (line) => {
82
+ if (line.type === 'add' || line.synthetic === true) return false;
83
+ return line.type !== 'note' && line.type !== 'noteSep';
84
+ };
85
+
86
+ const countsNew = (line) => line.type !== 'del';
87
+
88
+ const stampLineNumbers = (lines, hunk) => {
89
+ let oldNo = hunk && hunk.oldStart > 0 ? hunk.oldStart : 0;
90
+ let newNo = hunk && hunk.newStart > 0 ? hunk.newStart : 1;
91
+ return lines.map((line) => {
92
+ const next = { ...line };
93
+ if (line.type === 'note' || line.type === 'noteSep') return next;
94
+ if (countsOld(line)) {
95
+ if (oldNo > 0) next.oldNo = oldNo;
96
+ oldNo += 1;
97
+ }
98
+ if (countsNew(line)) {
99
+ next.newNo = newNo;
100
+ newNo += 1;
101
+ }
102
+ return next;
103
+ });
104
+ };
105
+
83
106
  const displayLines = (hunk, blockId, layout = 'unified', radius, overlay) => {
84
107
  const flat = flattenBlock(hunk, blockId, 'new');
85
108
  const source = overlay ? replaceAdds(flat, overlay, blockId) : flat;
109
+ const numbered = stampLineNumbers(source, hunk);
86
110
  const around = radius ?? DISPLAY_CONTEXT;
87
- const painted = attachInline(windowAroundBlock(source, blockId, around));
111
+ const painted = attachInline(windowAroundBlock(numbered, blockId, around));
88
112
  const alignRows = LAYOUT_ALIGN[layout];
89
113
  return alignRows ? alignRows(painted) : painted;
90
114
  };
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const { graphemes } = ansi;
3
+ const { graphemes } = require('../term/ansi.js');
5
4
 
6
5
  const LCS_CELL_LIMIT = 250000;
7
6
  const NOISY_TINY_SPANS = 6;
@@ -131,10 +130,9 @@ const opsSpans = (ops) => {
131
130
  newParts.push({ text: op.text, changed: op.type === 'add' });
132
131
  }
133
132
  }
134
- return {
135
- oldSpans: collapseSpans(oldParts),
136
- newSpans: collapseSpans(newParts),
137
- };
133
+ const oldSpans = collapseSpans(oldParts);
134
+ const newSpans = collapseSpans(newParts);
135
+ return { oldSpans, newSpans };
138
136
  };
139
137
 
140
138
  const joinAffix = (prefix, mid, suffix) => {
@@ -154,10 +152,9 @@ const diffTrimmed = (a, b, spansOfOps) => {
154
152
  const mid = whole ? wholeChanged(midA, midB) : spansOfOps(lcsOps(midA, midB));
155
153
  const prefix = a.slice(0, lo);
156
154
  const suffix = a.slice(hiA);
157
- return {
158
- oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
159
- newSpans: joinAffix(prefix, mid.newSpans, suffix),
160
- };
155
+ const oldSpans = joinAffix(prefix, mid.oldSpans, suffix);
156
+ const newSpans = joinAffix(prefix, mid.newSpans, suffix);
157
+ return { oldSpans, newSpans };
161
158
  };
162
159
 
163
160
  const diffTokens = (a, b) => {
@@ -221,10 +218,9 @@ const refineWordOps = (ops) => {
221
218
  if (dels.length) oldParts.push({ text: dels.join(''), changed: true });
222
219
  if (adds.length) newParts.push({ text: adds.join(''), changed: true });
223
220
  }
224
- return {
225
- oldSpans: collapseSpans(oldParts),
226
- newSpans: collapseSpans(newParts),
227
- };
221
+ const oldSpans = collapseSpans(oldParts);
222
+ const newSpans = collapseSpans(newParts);
223
+ return { oldSpans, newSpans };
228
224
  };
229
225
 
230
226
  const opRuns = (ops) => {
package/lib/diff/items.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const patch = require('./patch.js');
4
- const { splitHunk, formatHunkHeader, formatPatch } = patch;
3
+ const { splitHunk, formatHunkHeader, formatPatch } = require('./patch.js');
5
4
 
6
5
  const BINARY_ITEM = {
7
6
  hunk: null,
@@ -70,8 +69,11 @@ const itemsFromFiles = (files, origin) => {
70
69
 
71
70
  const refreshIndexPatches = (items, item) => {
72
71
  if (!item.hunk) return items;
73
- const isPeer = (other) =>
74
- other.hunk === item.hunk && other.file === item.file;
72
+ const isPeer = (other) => {
73
+ const sameHunk = other.hunk === item.hunk;
74
+ const sameFile = other.file === item.file;
75
+ return sameHunk && sameFile;
76
+ };
75
77
  const peers = items.filter(isPeer);
76
78
  const sides = {};
77
79
  for (const peer of peers) sides[peer.blockId] = indexSide(peer.origin);
package/lib/diff/patch.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const align = require('./align.js');
4
- const { isCtxType } = align;
3
+ const { clamp } = require('../common/utilities.js');
4
+ const { isCtxType } = require('./align.js');
5
5
 
6
6
  const LINE_MARK = { add: '+', del: '-' };
7
7
 
@@ -40,7 +40,11 @@ const flattenBlock = (hunk, blockId, contextSide) => {
40
40
  }
41
41
  const kept = sideOf(contextSide, line.blockId) === 'old' ? 'del' : 'add';
42
42
  if (line.type === kept) {
43
- out.push({ type: 'ctx', text: line.text, noNl: line.noNl });
43
+ const type = 'ctx';
44
+ const { text, noNl } = line;
45
+ const synthetic = true;
46
+ const row = { type, text, noNl, synthetic };
47
+ out.push(row);
44
48
  }
45
49
  }
46
50
  return out;
@@ -72,10 +76,42 @@ const formatPatch = (file, hunk, blockId, contextSide) => {
72
76
  return `${parts.join('\n')}\n`;
73
77
  };
74
78
 
79
+ const addBlockRange = (hunk, blockId) => {
80
+ let newLine = hunk.newStart;
81
+ let start = 0;
82
+ let count = 0;
83
+ for (const line of hunk.lines) {
84
+ if (line.type === 'del') {
85
+ if (line.blockId === blockId && start === 0) start = newLine;
86
+ continue;
87
+ }
88
+ if (line.type === 'add' && line.blockId === blockId) {
89
+ if (count === 0) start = newLine;
90
+ count += 1;
91
+ }
92
+ newLine += 1;
93
+ }
94
+ if (start === 0) start = hunk.newStart;
95
+ return { start, count };
96
+ };
97
+
98
+ const replaceBlockAdds = (content, hunk, blockId, text) => {
99
+ const range = addBlockRange(hunk, blockId);
100
+ const ended = content.endsWith('\n');
101
+ const body = ended ? content.slice(0, -1) : content;
102
+ const lines = content === '' ? [] : body.split('\n');
103
+ const from = clamp(range.start - 1, 0, lines.length);
104
+ const added = text === '' ? [] : text.split('\n');
105
+ lines.splice(from, range.count, ...added);
106
+ return lines.join('\n') + (ended ? '\n' : '');
107
+ };
108
+
75
109
  module.exports = {
76
110
  splitHunk,
77
111
  flattenBlock,
78
112
  countSides,
79
113
  formatHunkHeader,
80
114
  formatPatch,
115
+ addBlockRange,
116
+ replaceBlockAdds,
81
117
  };
package/lib/diff/unit.js CHANGED
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const align = require('./align.js');
4
- const { attachInline } = align;
5
- const files = require('../files.js');
6
- const { listPath } = files;
3
+ const { attachInline } = require('./align.js');
4
+ const { listPath } = require('../common/files.js');
7
5
 
8
6
  const LCS_CELLS = 4000000;
9
7
 
@@ -75,6 +73,16 @@ const rowsFromAdds = (items) => {
75
73
  return rows;
76
74
  };
77
75
 
76
+ const stampFileNumbers = (lines) => {
77
+ let newNo = 1;
78
+ for (const line of lines) {
79
+ if (line.type === 'del') continue;
80
+ line.newNo = newNo;
81
+ newNo += 1;
82
+ }
83
+ return lines;
84
+ };
85
+
78
86
  const unitLines = (text, items) => {
79
87
  let rows = splitFile(text);
80
88
  if (!rows.length) rows = rowsFromAdds(items);
@@ -95,11 +103,14 @@ const unitLines = (text, items) => {
95
103
  out.push(tagged('del', del.text, del));
96
104
  }
97
105
  if (!out.length) out.push(tagged('ctx', '', NO_EXTRA));
98
- return attachInline(out);
106
+ return stampFileNumbers(attachInline(out));
99
107
  };
100
108
 
101
- const sameBlock = (line, item) =>
102
- line.item === item && line.blockId === item.blockId;
109
+ const sameBlock = (line, item) => {
110
+ const sameItem = line.item === item;
111
+ const sameId = line.blockId === item.blockId;
112
+ return sameItem && sameId;
113
+ };
103
114
 
104
115
  const changedLineRange = (lines, item, belongs) => {
105
116
  let start = -1;
@@ -187,25 +198,33 @@ const zipOps = (prev, next) => {
187
198
  };
188
199
 
189
200
  const lcsOps = (prev, next) => {
190
- const dp = lcsTable(prev, next);
191
201
  const ops = [];
202
+ const limit = Math.min(prev.length, next.length);
203
+ let head = 0;
204
+ while (head < limit && prev[head] === next[head]) {
205
+ ops.push({ kind: 'eq', text: next[head] });
206
+ head += 1;
207
+ }
208
+ const left = head ? prev.slice(head) : prev;
209
+ const right = head ? next.slice(head) : next;
210
+ const dp = lcsTable(left, right);
192
211
  let i = 0;
193
212
  let j = 0;
194
- while (i < prev.length && j < next.length) {
195
- if (prev[i] === next[j]) {
196
- ops.push({ kind: 'eq', text: next[j] });
213
+ while (i < left.length && j < right.length) {
214
+ if (left[i] === right[j]) {
215
+ ops.push({ kind: 'eq', text: right[j] });
197
216
  i += 1;
198
217
  j += 1;
199
218
  } else if (dp[i + 1][j] >= dp[i][j + 1]) {
200
219
  ops.push({ kind: 'del' });
201
220
  i += 1;
202
221
  } else {
203
- ops.push({ kind: 'ins', text: next[j] });
222
+ ops.push({ kind: 'ins', text: right[j] });
204
223
  j += 1;
205
224
  }
206
225
  }
207
- for (; i < prev.length; i++) ops.push({ kind: 'del' });
208
- for (; j < next.length; j++) ops.push({ kind: 'ins', text: next[j] });
226
+ for (; i < left.length; i++) ops.push({ kind: 'del' });
227
+ for (; j < right.length; j++) ops.push({ kind: 'ins', text: right[j] });
209
228
  return ops;
210
229
  };
211
230
 
@@ -241,7 +260,7 @@ const pushDels = (out, base, at) => {
241
260
  return i;
242
261
  };
243
262
 
244
- const mergeEditRows = (base, nextTexts) => {
263
+ const mergeRows = (base, nextTexts) => {
245
264
  const texts = nextTexts.length ? nextTexts : [''];
246
265
  const prev = [];
247
266
  for (const row of base) if (row.type !== 'del') prev.push(row.text);
@@ -259,7 +278,17 @@ const mergeEditRows = (base, nextTexts) => {
259
278
  }
260
279
  pushDels(out, base, at);
261
280
  if (!out.length) out.push(tagged('ctx', '', NO_EXTRA));
262
- return attachEditSpans(attachInline(out));
281
+ return stampFileNumbers(attachEditSpans(attachInline(out)));
282
+ };
283
+
284
+ let merged = { base: null, text: '', rows: [] };
285
+
286
+ const mergeEditRows = (base, nextTexts) => {
287
+ const text = nextTexts.join('\n');
288
+ if (merged.base === base && merged.text === text) return merged.rows;
289
+ const rows = mergeRows(base, nextTexts);
290
+ merged = { base, text, rows };
291
+ return rows;
263
292
  };
264
293
 
265
294
  module.exports = {