reslop 0.2.4 → 0.2.5

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 (67) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +1 -2
  3. package/lib/capabilities.js +0 -4
  4. package/lib/clipboard.js +39 -1
  5. package/lib/deps-diff.js +0 -3
  6. package/lib/deps.js +144 -176
  7. package/lib/diff/inline.js +66 -1
  8. package/lib/diff/unit.js +11 -28
  9. package/lib/editor.js +69 -9
  10. package/lib/files.js +17 -16
  11. package/lib/git-deps.js +58 -95
  12. package/lib/github.js +7 -7
  13. package/lib/gitlab.js +3 -3
  14. package/lib/highlight/core.js +11 -0
  15. package/lib/highlight/dot.js +2 -12
  16. package/lib/highlight/log.js +4 -11
  17. package/lib/highlight/txt.js +2 -12
  18. package/lib/keys.js +22 -12
  19. package/lib/manifest.js +0 -4
  20. package/lib/npm-commands.js +1 -22
  21. package/lib/remote.js +1 -7
  22. package/lib/render/branches.js +4 -13
  23. package/lib/render/chrome.js +85 -31
  24. package/lib/render/commits.js +95 -207
  25. package/lib/render/diff.js +31 -1
  26. package/lib/render/files.js +7 -16
  27. package/lib/render/frame.js +95 -110
  28. package/lib/render/notes.js +71 -16
  29. package/lib/render/npm.js +104 -18
  30. package/lib/render/primitives.js +108 -45
  31. package/lib/render/unit.js +4 -1
  32. package/lib/report-parse.js +0 -1
  33. package/lib/report-run.js +0 -1
  34. package/lib/report-test.js +40 -47
  35. package/lib/report-tools.js +0 -7
  36. package/lib/review-merge.js +101 -131
  37. package/lib/review.js +64 -77
  38. package/lib/select.js +52 -0
  39. package/lib/session/actions.js +23 -22
  40. package/lib/session/branches.js +5 -3
  41. package/lib/session/change-actions.js +173 -146
  42. package/lib/session/commit.js +32 -13
  43. package/lib/session/compose.js +120 -77
  44. package/lib/session/dispatch.js +36 -21
  45. package/lib/session/edit-click.js +57 -0
  46. package/lib/session/files-pane.js +24 -24
  47. package/lib/session/items.js +69 -89
  48. package/lib/session/lifecycle.js +31 -96
  49. package/lib/session/load.js +51 -58
  50. package/lib/session/navigation.js +105 -96
  51. package/lib/session/npm.js +158 -77
  52. package/lib/session/ops.js +192 -51
  53. package/lib/session/pointer.js +11 -5
  54. package/lib/session/progress.js +9 -6
  55. package/lib/session/review.js +35 -54
  56. package/lib/session/tasks.js +292 -0
  57. package/lib/session/terminal.js +13 -17
  58. package/lib/session/unit.js +3 -4
  59. package/lib/session/update.js +76 -97
  60. package/lib/session/view.js +17 -14
  61. package/lib/session/watch.js +47 -27
  62. package/lib/session.js +34 -79
  63. package/lib/update.js +1 -5
  64. package/lib/utilities.js +21 -1
  65. package/lib/wrap.js +15 -0
  66. package/package.json +1 -1
  67. package/lib/session/todos.js +0 -293
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.2.5][] - 2026-09-30
6
+
7
+ - Remove UI and filesystem polling timers, refresh on events
8
+ - Refresh the commits list when HEAD changes outside reslop
9
+ - Update stage/unstage immediately, git/npm in the background
10
+ - Add editor hotkeys Ctrl+X, Ctrl+C, and Ctrl+V
11
+ - Improve diff calculation
12
+ - Improve npm command and test edit/run
13
+ - Refactor internals: simplify and add classes
14
+ - Hothey "i" add to ignore lists: .gitignore, .npmignore
15
+ - Rename TODO to tasks and improve TUI/TUX
16
+
5
17
  ## [0.2.4][] - 2026-09-28
6
18
 
7
19
  - Add AI instructions to run `npm t` or `reslop t -- node --test`
@@ -141,7 +153,8 @@
141
153
  - Feedback and todos that produce a repair plan for an agent
142
154
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
143
155
 
144
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...HEAD
156
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...HEAD
157
+ [0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
145
158
  [0.2.4]: https://github.com/tshemsedinov/reslop/compare/v0.2.3...v0.2.4
146
159
  [0.2.3]: https://github.com/tshemsedinov/reslop/compare/v0.2.1...v0.2.3
147
160
  [0.2.1]: https://github.com/tshemsedinov/reslop/compare/v0.2.0...v0.2.1
package/README.md CHANGED
@@ -11,8 +11,7 @@
11
11
 
12
12
  > Turn generated changes into owned changes
13
13
 
14
- Open a change, leave findings and a backlog, and prepare a repair plan
15
- for the agent.
14
+ Open a change, leave findings and a backlog, and prepare a repair plan for the agent.
16
15
 
17
16
  ```text
18
17
  Review → Plan → Repair → Verify
@@ -97,12 +97,8 @@ const sessionCapabilities = (options) => {
97
97
  };
98
98
 
99
99
  module.exports = {
100
- READ_METHODS,
101
- CHANGE_METHODS,
102
- BRANCH_METHODS,
103
100
  SOURCE_CAPS,
104
101
  capabilitiesFor,
105
- withReadOnly,
106
102
  attachCapabilities,
107
103
  sessionCapabilities,
108
104
  };
package/lib/clipboard.js CHANGED
@@ -29,8 +29,44 @@ const runClip = (cmd, args, text) => {
29
29
  return result.status === 0;
30
30
  };
31
31
 
32
+ const UNIX_PASTE = [
33
+ { cmd: 'wl-paste', args: ['--no-newline'] },
34
+ { cmd: 'xclip', args: ['-selection', 'clipboard', '-o'] },
35
+ { cmd: 'xsel', args: ['--clipboard', '--output'] },
36
+ ];
37
+
38
+ let copied = '';
39
+
40
+ const pasteTools = (platform = process.platform) => {
41
+ if (platform === 'win32') {
42
+ const args = ['-NoProfile', '-Command', 'Get-Clipboard'];
43
+ return [{ cmd: 'powershell', args }];
44
+ }
45
+ if (platform === 'darwin') return [{ cmd: 'pbpaste', args: [] }];
46
+ return UNIX_PASTE;
47
+ };
48
+
49
+ const readPaste = (cmd, args) => {
50
+ const result = spawnSync(
51
+ cmd,
52
+ args,
53
+ spawnBase({ encoding: 'utf8', timeout: CLIP_TIMEOUT_MS }),
54
+ );
55
+ if (result.error || result.status !== 0) return '';
56
+ return result.stdout || '';
57
+ };
58
+
59
+ const systemPaste = () => {
60
+ for (const tool of pasteTools()) {
61
+ const text = readPaste(tool.cmd, tool.args);
62
+ if (text) return text;
63
+ }
64
+ return '';
65
+ };
66
+
32
67
  const copyText = (text, stdout) => {
33
68
  if (!text) return false;
69
+ copied = text;
34
70
  let ok = false;
35
71
  if (stdout && stdout.write) {
36
72
  stdout.write(osc52(text));
@@ -42,4 +78,6 @@ const copyText = (text, stdout) => {
42
78
  return ok;
43
79
  };
44
80
 
45
- module.exports = { osc52, copyText };
81
+ const pasteText = () => systemPaste() || copied;
82
+
83
+ module.exports = { osc52, copyText, pasteText };
package/lib/deps-diff.js CHANGED
@@ -645,14 +645,11 @@ module.exports = {
645
645
  DEP_CAPTION,
646
646
  diffSections,
647
647
  mergeResolved,
648
- orderedChanges,
649
648
  summarizeGroup,
650
649
  collectGroups,
651
650
  takeManifestItems,
652
651
  foldedDepNames,
653
652
  stripFoldedDepItem,
654
- buildRows,
655
- makeHunk,
656
653
  makeDepItems,
657
654
  stubFileItem,
658
655
  };
package/lib/deps.js CHANGED
@@ -6,6 +6,8 @@ const metautil = require('metautil');
6
6
  const { jsonParse, isHashObject } = metautil;
7
7
  const files = require('./files.js');
8
8
  const { itemPath } = files;
9
+ const utilities = require('./utilities.js');
10
+ const { asText } = utilities;
9
11
  const manifest = require('./manifest.js');
10
12
  const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
11
13
  const { listFilePath, readSections, lockEntries, lockPackageCount } = manifest;
@@ -240,7 +242,7 @@ const viaNotes = (via, pkgName) => {
240
242
  const notes = [];
241
243
  for (const item of via) {
242
244
  if (!item || typeof item !== 'object') continue;
243
- const title = typeof item.title === 'string' ? item.title : '';
245
+ const title = asText(item.title);
244
246
  if (!title) continue;
245
247
  const hasName = typeof item.name === 'string' && item.name;
246
248
  const named = hasName ? item.name : pkgName;
@@ -271,32 +273,52 @@ const auditEntryNames = (key, entry) => {
271
273
  return names;
272
274
  };
273
275
 
274
- const keepHighestAudit = (audit, name, severity, fix, range, notes) => {
276
+ const uniqueTexts = (list) => [...new Set(list.filter(Boolean))];
277
+
278
+ const keepHighestAudit = (audit, found) => {
279
+ const { name, severity } = found;
275
280
  if (!name || !severity || severity === 'info') return;
276
- const titles = [];
277
- for (const note of notes) {
278
- if (note && !titles.includes(note)) titles.push(note);
279
- }
281
+ const titles = uniqueTexts(found.titles);
280
282
  const prev = audit.get(name);
281
283
  if (prev && severityRank(prev.severity) >= severityRank(severity)) {
282
- const merged = [...prev.titles];
283
- for (const note of titles) {
284
- if (!merged.includes(note)) merged.push(note);
285
- }
286
- const next = {
284
+ audit.set(name, {
287
285
  ...prev,
288
- titles: merged,
289
- fix: prev.fix || fix,
290
- range: prev.range || range,
291
- };
292
- audit.set(name, next);
286
+ titles: uniqueTexts([...prev.titles, ...titles]),
287
+ fix: prev.fix || found.fix,
288
+ range: prev.range || found.range,
289
+ });
293
290
  return;
294
291
  }
295
292
  const title = titles[0] || '';
296
- const next = { name, severity, title, titles, fix, range };
297
- next.fix ||= '';
298
- next.range ||= '';
299
- audit.set(name, next);
293
+ const fix = found.fix || '';
294
+ const range = found.range || '';
295
+ audit.set(name, { name, severity, title, titles, fix, range });
296
+ };
297
+
298
+ const isEntry = (value) => Boolean(value) && typeof value === 'object';
299
+
300
+ const vulnFindings = (vulns) => {
301
+ const found = [];
302
+ for (const [key, entry] of Object.entries(vulns)) {
303
+ if (!isEntry(entry)) continue;
304
+ const { severity } = entry;
305
+ const titles = viaNotes(entry.via, entry.name || key);
306
+ const fix = auditFix(entry);
307
+ const range = asText(entry.range);
308
+ for (const name of auditEntryNames(key, entry)) {
309
+ found.push({ name, severity, fix, range, titles });
310
+ }
311
+ }
312
+ return found;
313
+ };
314
+
315
+ const advisoryFinding = (entry) => {
316
+ const name = entry.module_name || entry.name;
317
+ const title = namedTitle(name, asText(entry.title));
318
+ const titles = title ? [title] : [];
319
+ const fix = auditFix(entry);
320
+ const range = asText(entry.range);
321
+ return { name, severity: entry.severity, fix, range, titles };
300
322
  };
301
323
 
302
324
  const parseAuditReport = (text) => {
@@ -304,36 +326,13 @@ const parseAuditReport = (text) => {
304
326
  if (!text || !`${text}`.trim()) return audit;
305
327
  const data = jsonParse(text);
306
328
  if (!isHashObject(data)) return audit;
307
- const vulns = data.vulnerabilities;
308
- if (vulns && typeof vulns === 'object') {
309
- for (const key of Object.keys(vulns)) {
310
- const entry = vulns[key];
311
- if (!entry || typeof entry !== 'object') continue;
312
- const severity = entry.severity;
313
- const names = auditEntryNames(key, entry);
314
- const pkgName = entry.name || key;
315
- const titles = viaNotes(entry.via, pkgName);
316
- const fix = auditFix(entry);
317
- const range = typeof entry.range === 'string' ? entry.range : '';
318
- for (const name of names) {
319
- keepHighestAudit(audit, name, severity, fix, range, titles);
320
- }
321
- }
322
- }
323
- const advisories = data.advisories;
324
- if (advisories && typeof advisories === 'object') {
325
- for (const entry of Object.values(advisories)) {
326
- if (!entry || typeof entry !== 'object') continue;
327
- const name = entry.module_name || entry.name;
328
- const severity = entry.severity;
329
- const raw = typeof entry.title === 'string' ? entry.title : '';
330
- const title = namedTitle(name, raw);
331
- const titles = title ? [title] : [];
332
- const fix = auditFix(entry);
333
- const range = typeof entry.range === 'string' ? entry.range : '';
334
- keepHighestAudit(audit, name, severity, fix, range, titles);
335
- }
329
+ const { vulnerabilities, advisories } = data;
330
+ const found = isEntry(vulnerabilities) ? vulnFindings(vulnerabilities) : [];
331
+ if (isEntry(advisories)) {
332
+ const listed = Object.values(advisories).filter(isEntry);
333
+ found.push(...listed.map(advisoryFinding));
336
334
  }
335
+ for (const entry of found) keepHighestAudit(audit, entry);
337
336
  return audit;
338
337
  };
339
338
 
@@ -345,11 +344,11 @@ const parseOutdatedReport = (text) => {
345
344
  for (const name of Object.keys(data)) {
346
345
  const entry = data[name];
347
346
  if (!entry || typeof entry !== 'object') continue;
348
- const current = typeof entry.current === 'string' ? entry.current : '';
349
- const wanted = typeof entry.wanted === 'string' ? entry.wanted : '';
350
- const latest = typeof entry.latest === 'string' ? entry.latest : '';
351
- const type = typeof entry.type === 'string' ? entry.type : '';
347
+ const wanted = asText(entry.wanted);
352
348
  if (!wanted) continue;
349
+ const current = asText(entry.current);
350
+ const latest = asText(entry.latest);
351
+ const type = asText(entry.type);
353
352
  outdated.set(name, { current, wanted, latest, type });
354
353
  }
355
354
  return outdated;
@@ -390,21 +389,18 @@ const unusedChangeNames = (changes, used) => {
390
389
  return names;
391
390
  };
392
391
 
393
- const unusedDeclaredNames = (pkg, used) => {
394
- const names = [];
395
- if (!pkgHasExportEntry(pkg)) return names;
396
- if (!used || !used.size) return names;
392
+ const unusedDeclared = (pkg, used) => {
393
+ const found = [];
394
+ if (!pkgHasExportEntry(pkg) || !used?.size) return found;
397
395
  const maps = readSections(pkg);
398
396
  for (const section of SECTION_KEYS) {
399
397
  if (section === 'peerDependencies') continue;
400
- const keys = [...maps[section].keys()];
401
- keys.sort();
402
- for (const name of keys) {
403
- if (used.has(name)) continue;
404
- names.push(name);
398
+ const map = maps[section];
399
+ for (const name of [...map.keys()].sort()) {
400
+ if (!used.has(name)) found.push({ name, section, from: map.get(name) });
405
401
  }
406
402
  }
407
- return names;
403
+ return found;
408
404
  };
409
405
 
410
406
  const markUnusedChanges = (changes, used, pkg, exportEntries) => {
@@ -418,92 +414,69 @@ const markUnusedChanges = (changes, used, pkg, exportEntries) => {
418
414
 
419
415
  const unusedDeclaredChanges = (pkg, used, exportEntries) => {
420
416
  const changes = [];
421
- if (!pkgHasExportEntry(pkg)) return changes;
422
- if (!used || !used.size) return changes;
423
- const maps = readSections(pkg);
424
417
  const taken = new Set();
425
- for (const section of SECTION_KEYS) {
426
- if (section === 'peerDependencies') continue;
427
- const map = maps[section];
428
- const names = [...map.keys()];
429
- names.sort();
430
- for (const name of names) {
431
- if (used.has(name)) continue;
432
- if (taken.has(name)) continue;
433
- if (!depExports(exportEntries, name)) continue;
434
- taken.add(name);
435
- const version = map.get(name);
436
- changes.push({
437
- name,
438
- section,
439
- action: 'removed',
440
- from: version,
441
- to: '',
442
- propose: true,
443
- unused: true,
444
- });
445
- }
418
+ for (const { name, section, from } of unusedDeclared(pkg, used)) {
419
+ if (taken.has(name) || !depExports(exportEntries, name)) continue;
420
+ taken.add(name);
421
+ const action = 'removed';
422
+ const to = '';
423
+ changes.push({
424
+ name,
425
+ section,
426
+ action,
427
+ from,
428
+ to,
429
+ propose: true,
430
+ unused: true,
431
+ });
446
432
  }
447
433
  return changes;
448
434
  };
449
435
 
436
+ const resolvedChange = (name, vul, resolved) => {
437
+ const from = resolved.get(name) || '';
438
+ if (!vul || !from) return null;
439
+ const to = proposedLockVersion(from, vul);
440
+ const section = 'resolved';
441
+ const action = 'vulnerable';
442
+ return { name, section, action, from, to, propose: true, audit: vul };
443
+ };
444
+
445
+ const declaredChange = (name, declared, out, vul) => {
446
+ const wanted = proposedWanted(out, vul);
447
+ if (!wanted && !vul) return null;
448
+ const from = declared.version;
449
+ const to = wanted ? applyWantedRange(from, wanted) : from;
450
+ if (to === from && !vul) return null;
451
+ const action = to === from ? 'vulnerable' : 'changed';
452
+ const { section } = declared;
453
+ const change = { name, section, action, from, to, propose: true };
454
+ if (out) change.outdated = out;
455
+ if (vul) change.audit = vul;
456
+ return change;
457
+ };
458
+
459
+ const mapKeys = (map) => (map ? [...map.keys()] : []);
460
+
450
461
  const buildProposedChanges = (pkg, outdated, audit, lock, options = {}) => {
451
- const used = options.used;
452
- const exportEntries = options.exportEntries;
453
- const names = new Set();
454
- if (outdated) {
455
- for (const name of outdated.keys()) names.add(name);
456
- }
457
- if (audit) {
458
- for (const name of audit.keys()) names.add(name);
459
- }
460
- const listed = [...names];
462
+ const listed = [...new Set([...mapKeys(outdated), ...mapKeys(audit)])];
461
463
  listed.sort();
462
464
  const resolved = lockEntries(lock);
463
- const unused = unusedDeclaredChanges(pkg, used, exportEntries);
464
- const taken = new Set();
465
- for (const change of unused) taken.add(change.name);
466
- const changes = [...unused];
465
+ const changes = unusedDeclaredChanges(
466
+ pkg,
467
+ options.used,
468
+ options.exportEntries,
469
+ );
470
+ const taken = new Set(changes.map((change) => change.name));
467
471
  for (const name of listed) {
468
472
  if (taken.has(name)) continue;
469
473
  const out = outdated ? outdated.get(name) : null;
470
474
  const vul = audit ? audit.get(name) : null;
471
- const type = out ? out.type : '';
472
- const declared = findDeclared(pkg, name, type);
473
- if (!declared) {
474
- if (!vul) continue;
475
- const version = resolved.get(name) || '';
476
- if (!version) continue;
477
- const to = proposedLockVersion(version, vul);
478
- changes.push({
479
- name,
480
- section: 'resolved',
481
- action: 'vulnerable',
482
- from: version,
483
- to,
484
- propose: true,
485
- audit: vul,
486
- });
487
- continue;
488
- }
489
- const wanted = proposedWanted(out, vul);
490
- if (!wanted && !vul) continue;
491
- let to = declared.version;
492
- if (wanted) to = applyWantedRange(declared.version, wanted);
493
- if (to === declared.version && !vul) continue;
494
- let action = 'changed';
495
- if (to === declared.version) action = 'vulnerable';
496
- const change = {
497
- name,
498
- section: declared.section,
499
- action,
500
- from: declared.version,
501
- to,
502
- propose: true,
503
- };
504
- if (out) change.outdated = out;
505
- if (vul) change.audit = vul;
506
- changes.push(change);
475
+ const declared = findDeclared(pkg, name, out ? out.type : '');
476
+ const change = declared
477
+ ? declaredChange(name, declared, out, vul)
478
+ : resolvedChange(name, vul, resolved);
479
+ if (change) changes.push(change);
507
480
  }
508
481
  return changes;
509
482
  };
@@ -561,6 +534,30 @@ const insertAfterPath = (items, added, rel) => {
561
534
  return [...items.slice(0, at + 1), ...added, ...items.slice(at + 1)];
562
535
  };
563
536
 
537
+ const placeFolded = (items, { folded, synthetic, leftoverNames }) => {
538
+ const out = [];
539
+ const slot = new Map();
540
+ for (const item of items) {
541
+ const meta = depFileMeta(itemPath(item));
542
+ const pkgRel = meta ? relOf(meta.dir, MANIFEST) : '';
543
+ const key = meta ? `${item.origin}\0${meta.dir}` : '';
544
+ if (meta && synthetic.has(key) && !slot.has(pkgRel)) {
545
+ slot.set(pkgRel, out.length);
546
+ }
547
+ if (folded.has(item)) continue;
548
+ if (!meta || meta.kind !== 'manifest') {
549
+ out.push(item);
550
+ continue;
551
+ }
552
+ const names = leftoverNames.get(key);
553
+ const kept = names ? stripFoldedDepItem(item, names) : item;
554
+ if (!kept) continue;
555
+ out.push(kept);
556
+ slot.set(pkgRel, out.length);
557
+ }
558
+ return spliceAtSlots(out, synthetic, slot);
559
+ };
560
+
564
561
  const foldDepItems = (
565
562
  items,
566
563
  readSides = () => ({ oldText: '', newText: '' }),
@@ -578,51 +575,22 @@ const foldDepItems = (
578
575
  const summary = summarizeGroup(group, readSides);
579
576
  const unusedNames = unusedChangeNames(summary.changes, usedNames);
580
577
  const exportEntries = loadExportEntries(unusedNames, extra, group.dir);
581
- const unused = markUnusedChanges(
582
- summary.changes,
583
- usedNames,
584
- summary.pkg,
585
- exportEntries,
586
- );
587
- const audited = markChanges(unused, audit, 'audit');
588
- const changes = markChanges(audited, outdated, 'outdated');
578
+ const { pkg } = summary;
579
+ let changes = summary.changes;
580
+ changes = markUnusedChanges(changes, usedNames, pkg, exportEntries);
581
+ changes = markChanges(changes, audit, 'audit');
582
+ changes = markChanges(changes, outdated, 'outdated');
589
583
  const next = { ...summary, changes };
590
- leftoverNames.set(key, foldedDepNames(next.changes));
584
+ leftoverNames.set(key, foldedDepNames(changes));
591
585
  const taken = takeManifestItems(next);
592
- if (next.foldLock) {
593
- for (const item of next.lockItems) taken.push(item);
594
- }
595
- if (!taken.length && !next.changes.length) continue;
586
+ if (next.foldLock) taken.push(...next.lockItems);
587
+ if (!taken.length && !changes.length) continue;
596
588
  const made = makeDepItems(group.origin, group.items, taken, next);
597
589
  if (made.length) synthetic.set(key, made);
598
590
  for (const item of taken) folded.add(item);
599
591
  }
600
592
  if (!synthetic.size && !folded.size) return items;
601
- const out = [];
602
- const slot = new Map();
603
- for (const item of items) {
604
- const rel = itemPath(item);
605
- const meta = depFileMeta(rel);
606
- if (meta) {
607
- const pkgRel = relOf(meta.dir, MANIFEST);
608
- const key = `${item.origin}\0${meta.dir}`;
609
- if (synthetic.has(key) && !slot.has(pkgRel)) {
610
- slot.set(pkgRel, out.length);
611
- }
612
- }
613
- if (folded.has(item)) continue;
614
- if (meta && meta.kind === 'manifest') {
615
- const names = leftoverNames.get(`${item.origin}\0${meta.dir}`);
616
- const kept = names ? stripFoldedDepItem(item, names) : item;
617
- if (kept) {
618
- out.push(kept);
619
- slot.set(relOf(meta.dir, MANIFEST), out.length);
620
- }
621
- continue;
622
- }
623
- out.push(item);
624
- }
625
- return spliceAtSlots(out, synthetic, slot);
593
+ return placeFolded(items, { folded, synthetic, leftoverNames });
626
594
  };
627
595
 
628
596
  const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
@@ -630,7 +598,7 @@ const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
630
598
  if (!pkg) return [];
631
599
  const lock = jsonParse(extra.lockText ?? '');
632
600
  const used = extra.usedNames;
633
- const names = unusedDeclaredNames(pkg, used);
601
+ const names = unusedDeclared(pkg, used).map((dep) => dep.name);
634
602
  const exportEntries = loadExportEntries(names, extra, extra.dir);
635
603
  const options = { used, exportEntries };
636
604
  const changes = buildProposedChanges(pkg, outdated, audit, lock, options);
@@ -6,6 +6,7 @@ const { graphemes } = ansi;
6
6
  const LCS_CELL_LIMIT = 250000;
7
7
  const NOISY_TINY_SPANS = 6;
8
8
  const WORD_REFINE_MIN = 0.6;
9
+ const WHOLE_MIN_SHARED = 0.25;
9
10
  const PIECE_RE = /[A-Za-z_$][\w$]*|\d+|\s+|./g;
10
11
  const CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+|./g;
11
12
 
@@ -280,6 +281,65 @@ const spansFromWordOps = (ops) => {
280
281
  };
281
282
  };
282
283
 
284
+ const opRuns = (ops) => {
285
+ const runs = [];
286
+ for (const op of ops) {
287
+ const eq = op.type === 'eq';
288
+ let run = runs[runs.length - 1];
289
+ if (!run || run.eq !== eq) {
290
+ run = { eq, del: [], add: [] };
291
+ runs.push(run);
292
+ }
293
+ if (op.type !== 'add') run.del.push(op.text);
294
+ if (op.type !== 'del') run.add.push(op.text);
295
+ }
296
+ return runs;
297
+ };
298
+
299
+ const textSize = (parts) => parts.join('').length;
300
+
301
+ const editSize = (run) => Math.max(textSize(run.del), textSize(run.add));
302
+
303
+ const absorbTinyEquals = (runs) => {
304
+ for (let k = 1; k < runs.length - 1; k++) {
305
+ const [before, eq, after] = runs.slice(k - 1, k + 2);
306
+ if (!eq.eq) continue;
307
+ const size = textSize(eq.del);
308
+ if (size > editSize(before) || size > editSize(after)) continue;
309
+ const del = [...before.del, ...eq.del, ...after.del];
310
+ const add = [...before.add, ...eq.add, ...after.add];
311
+ runs.splice(k - 1, 3, { eq: false, del, add });
312
+ k = 0;
313
+ }
314
+ return runs;
315
+ };
316
+
317
+ const runOps = (runs) => {
318
+ const ops = [];
319
+ for (const run of runs) {
320
+ if (run.eq) {
321
+ for (const text of run.del) ops.push({ type: 'eq', text });
322
+ continue;
323
+ }
324
+ for (const text of run.del) ops.push({ type: 'del', text });
325
+ for (const text of run.add) ops.push({ type: 'add', text });
326
+ }
327
+ return ops;
328
+ };
329
+
330
+ const cleanOps = (ops) => runOps(absorbTinyEquals(opRuns(ops)));
331
+
332
+ const sharedShare = (spans) => {
333
+ let shared = 0;
334
+ let total = 0;
335
+ for (const span of spans) {
336
+ const size = span.text.replace(/\s/g, '').length;
337
+ total += size;
338
+ if (!span.changed) shared += size;
339
+ }
340
+ return total === 0 ? 1 : shared / total;
341
+ };
342
+
283
343
  const diffChars = (oldText, newText) => {
284
344
  const a = splitPieces(oldText);
285
345
  const b = splitPieces(newText);
@@ -296,10 +356,15 @@ const diffChars = (oldText, newText) => {
296
356
  } else if (midA.length * midB.length > LCS_CELL_LIMIT) {
297
357
  mid = wholeChanged(midA, midB);
298
358
  } else {
299
- mid = spansFromWordOps(lcsOps(midA, midB));
359
+ mid = spansFromWordOps(cleanOps(lcsOps(midA, midB)));
300
360
  }
301
361
  const oldSpans = joinAffix(prefix, mid.oldSpans, suffix);
302
362
  const newSpans = joinAffix(prefix, mid.newSpans, suffix);
363
+ const oldShare = sharedShare(oldSpans);
364
+ const newShare = sharedShare(newSpans);
365
+ if (oldShare < WHOLE_MIN_SHARED && newShare < WHOLE_MIN_SHARED) {
366
+ return wholeChanged(a, b);
367
+ }
303
368
  return { oldSpans, newSpans };
304
369
  };
305
370
 
package/lib/diff/unit.js CHANGED
@@ -128,43 +128,26 @@ const sameBlock = (line, item) => {
128
128
  return line.blockId === item.blockId;
129
129
  };
130
130
 
131
- const blockLineRange = (lines, item) => {
132
- let start = 0;
131
+ const changedLineRange = (lines, item, belongs) => {
132
+ let start = -1;
133
133
  let end = 0;
134
- let found = false;
135
- if (!item) return { start, end };
134
+ if (!item) return { start: 0, end };
136
135
  for (let i = 0; i < lines.length; i++) {
137
136
  const line = lines[i];
138
- if (!sameBlock(line, item)) continue;
139
- if (line.type === 'ctx') continue;
140
- if (!found) {
141
- start = i;
142
- found = true;
143
- }
137
+ if (line.type === 'ctx' || !belongs(line, item)) continue;
138
+ if (start < 0) start = i;
144
139
  end = i;
145
140
  }
146
- return { start, end };
141
+ return { start: Math.max(start, 0), end };
147
142
  };
148
143
 
144
+ const blockLineRange = (lines, item) =>
145
+ changedLineRange(lines, item, sameBlock);
146
+
149
147
  const blockLineIndex = (lines, item) => blockLineRange(lines, item).start;
150
148
 
151
- const hunkLineRange = (lines, item) => {
152
- let start = 0;
153
- let end = 0;
154
- let found = false;
155
- if (!item) return { start, end };
156
- for (let i = 0; i < lines.length; i++) {
157
- const line = lines[i];
158
- if (!sameHunk(line.item, item)) continue;
159
- if (line.type === 'ctx') continue;
160
- if (!found) {
161
- start = i;
162
- found = true;
163
- }
164
- end = i;
165
- }
166
- return { start, end };
167
- };
149
+ const hunkLineRange = (lines, item) =>
150
+ changedLineRange(lines, item, (line) => sameHunk(line.item, item));
168
151
 
169
152
  const fileOffset = (lines, at) => {
170
153
  let offset = 0;