reslop 0.2.5 → 0.2.7

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
package/lib/utilities.js CHANGED
@@ -9,16 +9,12 @@ const IS_WIN = process.platform === 'win32';
9
9
  const DEFAULT_ENCODING = 'utf8';
10
10
  const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024;
11
11
 
12
- const UNIX_CLIP = [
13
- { cmd: 'wl-copy', args: [] },
14
- { cmd: 'xclip', args: ['-selection', 'clipboard'] },
15
- { cmd: 'xsel', args: ['--clipboard', '--input'] },
16
- ];
17
-
18
12
  const parseVersion = (text) => {
19
13
  const match = /^(\d+)\.(\d+)\.(\d+)/.exec(`${text ?? ''}`);
20
14
  if (!match) return null;
21
- const [, major, minor, patch] = match.map(Number);
15
+ const major = Number(match[1]);
16
+ const minor = Number(match[2]);
17
+ const patch = Number(match[3]);
22
18
  return { major, minor, patch };
23
19
  };
24
20
 
@@ -47,20 +43,13 @@ const dateStamp = (date) => {
47
43
  return `${date.getFullYear()}-${month}-${pad2(date.getDate())}`;
48
44
  };
49
45
 
50
- const npmBin = (platform = process.platform) =>
51
- platform === 'win32' ? 'npm.cmd' : 'npm';
46
+ const npmBin = () => (IS_WIN ? 'npm.cmd' : 'npm');
52
47
 
53
48
  const spawnBase = (extra = {}) => ({ windowsHide: true, ...extra });
54
49
 
55
50
  const npmOpts = (extra = {}, platform = process.platform) =>
56
51
  spawnBase({ shell: platform === 'win32', ...extra });
57
52
 
58
- const clipTools = (platform = process.platform) => {
59
- if (platform === 'win32') return [{ cmd: 'clip', args: [] }];
60
- if (platform === 'darwin') return [{ cmd: 'pbcopy', args: [] }];
61
- return UNIX_CLIP;
62
- };
63
-
64
53
  const listen = (emitter, event, handler) => {
65
54
  if (!emitter || typeof emitter.on !== 'function') return () => {};
66
55
  emitter.on(event, handler);
@@ -91,69 +80,36 @@ const watchResize = (stdout, proc, onResize, signal) => {
91
80
  return stop;
92
81
  };
93
82
 
94
- const abortError = () => {
95
- const error = new Error('aborted');
96
- error.code = 'ABORT';
97
- return error;
98
- };
83
+ const codeError = (message, code) =>
84
+ Object.assign(new Error(message), { code });
99
85
 
100
- const isAbort = (error) => !!(error && error.code === 'ABORT');
86
+ const abortError = () => codeError('aborted', 'ABORT');
101
87
 
102
- const asOutput = (value) => {
103
- if (value === undefined || value === null) return '';
104
- if (typeof value === 'string') return value;
105
- return `${value}`;
106
- };
88
+ const isAbort = (error) => !!(error && error.code === 'ABORT');
107
89
 
108
- const procResult = (values = {}) => {
109
- const status = values.status ?? null;
110
- const stdout = asOutput(values.stdout);
111
- const stderr = asOutput(values.stderr);
112
- const error = values.error ?? null;
113
- return { status, stdout, stderr, error };
114
- };
90
+ const procResult = (values = {}) => ({
91
+ status: values.status ?? null,
92
+ stdout: `${values.stdout ?? ''}`,
93
+ stderr: `${values.stderr ?? ''}`,
94
+ error: values.error ?? null,
95
+ });
115
96
 
116
- const spawnOptions = (options = {}) => {
117
- const encoding = options.encoding ?? DEFAULT_ENCODING;
118
- const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
97
+ const runProcSync = (cmd, args, options = {}) => {
119
98
  const opts = spawnBase({
120
99
  cwd: options.cwd,
121
100
  env: options.env,
122
- encoding,
101
+ encoding: options.encoding ?? DEFAULT_ENCODING,
123
102
  input: options.input,
124
- maxBuffer,
103
+ maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER,
125
104
  timeout: options.timeout,
126
105
  });
127
106
  if (options.shell) opts.shell = true;
128
- return opts;
129
- };
130
-
131
- const runProcSync = (cmd, args, options = {}) => {
132
- const result = spawnSync(cmd, args, spawnOptions(options));
133
- return procResult(result);
134
- };
135
-
136
- const attachOutput = (stream, encoding, onChunk) => {
137
- if (!stream) return;
138
- stream.setEncoding(encoding);
139
- stream.on('data', onChunk);
140
- };
141
-
142
- const writeInput = (child, input) => {
143
- if (!child.stdin) return;
144
- try {
145
- if (input) child.stdin.end(input);
146
- else child.stdin.end();
147
- } catch {
148
- // closed
149
- }
107
+ return procResult(spawnSync(cmd, args, opts));
150
108
  };
151
109
 
152
110
  const listenChild = (child, options, resolve) => {
153
111
  const encoding = options.encoding ?? DEFAULT_ENCODING;
154
112
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
155
- const timeout = options.timeout;
156
- const input = options.input;
157
113
  const signal = options.signal;
158
114
  let stdout = '';
159
115
  let stderr = '';
@@ -163,46 +119,49 @@ const listenChild = (child, options, resolve) => {
163
119
  const finish = (result) => {
164
120
  if (settled) return;
165
121
  settled = true;
166
- if (timer) clearTimeout(timer);
167
- timer = null;
168
- if (signal && onAbort) {
169
- signal.removeEventListener('abort', onAbort);
170
- }
171
- onAbort = null;
122
+ clearTimeout(timer);
123
+ if (signal) signal.removeEventListener('abort', onAbort);
172
124
  resolve(result);
173
125
  };
174
- onAbort = () => {
126
+ const fail = (error) => {
175
127
  child.kill();
176
- finish(procResult({ stdout, stderr, error: abortError() }));
128
+ finish(procResult({ stdout, stderr, error }));
177
129
  };
178
- const append = (kind, chunk) => {
179
- if (kind === 'out') stdout += chunk;
130
+ onAbort = () => fail(abortError());
131
+ const append = (chunk, isOut) => {
132
+ if (isOut) stdout += chunk;
180
133
  else stderr += chunk;
181
- if (stdout.length + stderr.length <= maxBuffer) return;
182
- child.kill();
183
- const error = new Error('maxBuffer exceeded');
184
- error.code = 'ENOBUFS';
185
- finish(procResult({ stdout, stderr, error }));
134
+ if (stdout.length + stderr.length > maxBuffer) {
135
+ fail(codeError('maxBuffer exceeded', 'ENOBUFS'));
136
+ }
186
137
  };
187
138
  if (signal) signal.addEventListener('abort', onAbort);
188
- if (timeout) {
189
- timer = setTimeout(() => {
190
- child.kill();
191
- const error = new Error('timed out');
192
- error.code = 'ETIMEDOUT';
193
- finish(procResult({ stdout, stderr, error }));
194
- }, timeout);
139
+ if (options.timeout) {
140
+ const onTimeout = () => fail(codeError('timed out', 'ETIMEDOUT'));
141
+ timer = setTimeout(onTimeout, options.timeout);
195
142
  }
196
143
  if (signal && signal.aborted) onAbort();
197
- attachOutput(child.stdout, encoding, (chunk) => append('out', chunk));
198
- attachOutput(child.stderr, encoding, (chunk) => append('err', chunk));
144
+ if (child.stdout) {
145
+ child.stdout.setEncoding(encoding);
146
+ child.stdout.on('data', (chunk) => append(chunk, true));
147
+ }
148
+ if (child.stderr) {
149
+ child.stderr.setEncoding(encoding);
150
+ child.stderr.on('data', (chunk) => append(chunk, false));
151
+ }
199
152
  child.on('error', (error) => {
200
153
  finish(procResult({ stdout, stderr, error }));
201
154
  });
202
155
  child.on('close', (status) => {
203
156
  finish(procResult({ status, stdout, stderr }));
204
157
  });
205
- writeInput(child, input);
158
+ if (!child.stdin) return;
159
+ try {
160
+ if (options.input) child.stdin.end(options.input);
161
+ else child.stdin.end();
162
+ } catch {
163
+ // closed
164
+ }
206
165
  };
207
166
 
208
167
  const runProc = (cmd, args, options = {}) =>
@@ -238,7 +197,6 @@ module.exports = {
238
197
  npmBin,
239
198
  spawnBase,
240
199
  npmOpts,
241
- clipTools,
242
200
  listen,
243
201
  watchResize,
244
202
  abortError,
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const files = require('./files.js');
7
+ const { REVIEW_DIR } = files;
8
+ const review = require('./review.js');
9
+ const { listReviewNames, latestReviewName, loadReview, noteCounts } = review;
10
+ const worktree = require('./git-worktree.js');
11
+ const { runGitAsync, countUntrackedAdded } = worktree;
12
+ const model = require('./dashboard/model.js');
13
+ const { runName } = model;
14
+ const runs = require('./runs.js');
15
+ const { readRuns } = runs;
16
+
17
+ const emptyDiff = () => ({
18
+ stagedAdded: 0,
19
+ unstagedAdded: 0,
20
+ stagedRemoved: 0,
21
+ unstagedRemoved: 0,
22
+ });
23
+
24
+ const isRepoDir = (dir) => {
25
+ try {
26
+ fs.accessSync(path.join(dir, '.git'));
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ };
32
+
33
+ const listChildRepos = (dir) => {
34
+ let entries;
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ } catch {
38
+ return [];
39
+ }
40
+ const repos = [];
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
43
+ const full = path.join(dir, entry.name);
44
+ if (!isRepoDir(full)) continue;
45
+ repos.push({ name: entry.name, dir: full });
46
+ }
47
+ repos.sort((left, right) => left.name.localeCompare(right.name, 'en'));
48
+ return repos;
49
+ };
50
+
51
+ const workspaceAt = (dir) => {
52
+ let stat;
53
+ try {
54
+ stat = fs.statSync(dir);
55
+ } catch {
56
+ return null;
57
+ }
58
+ if (!stat.isDirectory() || isRepoDir(dir)) return null;
59
+ const repos = listChildRepos(dir);
60
+ if (!repos.length) return null;
61
+ return { root: path.resolve(dir), repos };
62
+ };
63
+
64
+ const workspaceFromArgs = (cwd, paths) => {
65
+ if (paths.length > 1) return null;
66
+ const dir = paths.length ? path.resolve(cwd, paths[0]) : cwd;
67
+ if (paths.length) {
68
+ try {
69
+ if (!fs.statSync(dir).isDirectory()) return null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ return workspaceAt(dir);
75
+ };
76
+
77
+ const sumNumstat = (text) => {
78
+ let added = 0;
79
+ let removed = 0;
80
+ for (const line of `${text ?? ''}`.split('\n')) {
81
+ if (!line) continue;
82
+ const parts = line.split('\t');
83
+ const add = Number(parts[0]);
84
+ const del = Number(parts[1]);
85
+ if (Number.isFinite(add)) added += add;
86
+ if (Number.isFinite(del)) removed += del;
87
+ }
88
+ return { added, removed };
89
+ };
90
+
91
+ const readNumstat = async (dir, cached) => {
92
+ const args = cached
93
+ ? ['diff', '--cached', '--numstat']
94
+ : ['diff', '--numstat'];
95
+ try {
96
+ const result = await runGitAsync(args, dir);
97
+ if (result.status !== 0) return { added: 0, removed: 0 };
98
+ return sumNumstat(result.stdout);
99
+ } catch {
100
+ return { added: 0, removed: 0 };
101
+ }
102
+ };
103
+
104
+ const readDiffStat = async (dir) => {
105
+ const staged = await readNumstat(dir, true);
106
+ const open = await readNumstat(dir, false);
107
+ const untracked = await countUntrackedAdded(dir);
108
+ return {
109
+ stagedAdded: staged.added,
110
+ unstagedAdded: open.added + untracked,
111
+ stagedRemoved: staged.removed,
112
+ unstagedRemoved: open.removed,
113
+ };
114
+ };
115
+
116
+ const countCommits = async (dir) => {
117
+ try {
118
+ const result = await runGitAsync(['rev-list', '--count', 'HEAD'], dir);
119
+ if (result.status !== 0) return 0;
120
+ const value = parseInt(result.stdout.trim(), 10);
121
+ return Number.isFinite(value) ? value : 0;
122
+ } catch {
123
+ return 0;
124
+ }
125
+ };
126
+
127
+ const taskStats = (dir) => {
128
+ const latest = latestReviewName(listReviewNames(dir));
129
+ if (!latest) return { done: 0, total: 0 };
130
+ try {
131
+ const file = path.join(dir, REVIEW_DIR, latest);
132
+ const counts = noteCounts(loadReview(file, []));
133
+ return { done: counts.tasksDone, total: counts.tasks };
134
+ } catch {
135
+ return { done: 0, total: 0 };
136
+ }
137
+ };
138
+
139
+ const pickRun = (dir) => {
140
+ const list = readRuns(dir);
141
+ const live = list.find((row) => row.status === 'running');
142
+ const record = live ?? list[0] ?? null;
143
+ if (!record) return null;
144
+ const progress = record.progress ?? {};
145
+ return {
146
+ name: runName(record),
147
+ status: record.status,
148
+ done: progress.done ?? 0,
149
+ failed: progress.failed ?? 0,
150
+ expected: progress.expected ?? 0,
151
+ result: record.result ?? null,
152
+ };
153
+ };
154
+
155
+ module.exports = {
156
+ emptyDiff,
157
+ isRepoDir,
158
+ listChildRepos,
159
+ workspaceAt,
160
+ workspaceFromArgs,
161
+ sumNumstat,
162
+ readDiffStat,
163
+ countCommits,
164
+ taskStats,
165
+ pickRun,
166
+ };
package/lib/wrap.js CHANGED
@@ -12,53 +12,37 @@ const wrapPlain = (text, width) => {
12
12
  let col = 0;
13
13
  let lastBreak = -1;
14
14
  let i = 0;
15
- const slice = (from, to) => {
16
- let out = '';
17
- for (let k = from; k < to; k++) out += parts[k];
18
- return out;
19
- };
20
- const flush = (end) => {
21
- rows.push(slice(start, end));
22
- start = end;
23
- col = 0;
24
- lastBreak = -1;
25
- };
26
15
  while (i < parts.length) {
27
16
  const ch = parts[i];
28
17
  const w = graphemeWidth(ch, col);
29
18
  if (col + w > width && col > 0) {
30
- if (lastBreak >= start) {
31
- flush(lastBreak + 1);
32
- i = start;
33
- continue;
34
- }
35
- flush(i);
19
+ const end = lastBreak >= start ? lastBreak + 1 : i;
20
+ rows.push(parts.slice(start, end).join(''));
21
+ start = end;
22
+ i = end;
23
+ col = 0;
24
+ lastBreak = -1;
36
25
  continue;
37
26
  }
38
27
  col += w;
39
28
  if (ch === ' ' || ch === '\t') lastBreak = i;
40
29
  i += 1;
41
30
  }
42
- rows.push(slice(start, parts.length));
31
+ rows.push(parts.slice(start).join(''));
43
32
  return rows;
44
33
  };
45
34
 
46
35
  const wrapDoc = (text, width) => {
47
36
  const rows = [];
48
- const parts = text.split('\n');
49
37
  let offset = 0;
50
- for (let p = 0; p < parts.length; p++) {
51
- const part = parts[p];
52
- const wrapped = wrapPlain(part, width);
53
- let used = 0;
54
- for (const row of wrapped) {
55
- rows.push({ start: offset + used, text: row });
56
- used += row.length;
38
+ for (const part of text.split('\n')) {
39
+ let start = offset;
40
+ for (const row of wrapPlain(part, width)) {
41
+ rows.push({ start, text: row });
42
+ start += row.length;
57
43
  }
58
- offset += part.length;
59
- if (p < parts.length - 1) offset += 1;
44
+ offset += part.length + 1;
60
45
  }
61
- if (!rows.length) rows.push({ start: 0, text: '' });
62
46
  return rows;
63
47
  };
64
48
 
@@ -69,9 +53,8 @@ const cursorInWrap = (text, cursor, width) => {
69
53
  const rows = wrapDoc(text, width);
70
54
  const clamped = Math.max(0, Math.min(cursor, text.length));
71
55
  let row = rows.length - 1;
72
- for (let i = 0; i < rows.length; i++) {
73
- const next = i + 1 < rows.length ? rows[i + 1].start : text.length + 1;
74
- if (clamped < next) {
56
+ for (let i = 0; i < rows.length - 1; i++) {
57
+ if (clamped < rows[i + 1].start) {
75
58
  row = i;
76
59
  break;
77
60
  }
@@ -81,30 +64,25 @@ const cursorInWrap = (text, cursor, width) => {
81
64
  return { row, col: visibleWidth(line.text.slice(0, take)) };
82
65
  };
83
66
 
84
- const indexAtRow = (start, text, col) => {
85
- const raw = `${text ?? ''}`;
86
- if (col <= 0) return start;
67
+ const colIndex = (text, col) => {
87
68
  let used = 0;
88
- let idx = 0;
89
- for (const ch of graphemes(raw)) {
69
+ let index = 0;
70
+ for (const ch of graphemes(text)) {
90
71
  const w = graphemeWidth(ch, used);
91
72
  if (used + w > col) break;
92
73
  used += w;
93
- idx += ch.length;
74
+ index += ch.length;
94
75
  }
95
- return start + idx;
76
+ return index;
77
+ };
78
+
79
+ const indexAtRow = (start, text, col) => {
80
+ if (col <= 0) return start;
81
+ return start + colIndex(`${text ?? ''}`, col);
96
82
  };
97
83
 
98
84
  const indexAtWrapCol = (row, col, limit) => {
99
- let used = 0;
100
- let idx = 0;
101
- for (const ch of graphemes(row.text)) {
102
- const w = graphemeWidth(ch, used);
103
- if (used + w > col) break;
104
- used += w;
105
- idx += ch.length;
106
- }
107
- const pos = row.start + idx;
85
+ const pos = row.start + colIndex(row.text, col);
108
86
  if (pos < limit) return pos;
109
87
  if (pos <= row.start) return row.start;
110
88
  const parts = graphemes(row.text);
@@ -114,15 +92,11 @@ const indexAtWrapCol = (row, col, limit) => {
114
92
  const wrapMove = (text, cursor, width, delta, wantCol) => {
115
93
  const rows = wrapDoc(text, width);
116
94
  const pos = cursorInWrap(text, cursor, width);
117
- let next = pos.row + delta;
118
- if (next < 0) next = 0;
119
- if (next >= rows.length) next = rows.length - 1;
95
+ const next = Math.min(Math.max(pos.row + delta, 0), rows.length - 1);
120
96
  const col = wantCol ?? pos.col;
121
97
  if (next === pos.row) return { cursor, col };
122
- const row = rows[next];
123
- let limit = text.length + 1;
124
- if (next + 1 < rows.length) limit = rows[next + 1].start;
125
- return { cursor: indexAtWrapCol(row, col, limit), col };
98
+ const limit = next + 1 < rows.length ? rows[next + 1].start : text.length + 1;
99
+ return { cursor: indexAtWrapCol(rows[next], col, limit), col };
126
100
  };
127
101
 
128
102
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",