reslop 0.1.8 → 0.2.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 (95) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +27 -38
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +6 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +10 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/deps-diff.js +712 -0
  10. package/lib/deps.js +556 -32
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +34 -8
  20. package/lib/{git/branches.js → git-branches.js} +103 -10
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +592 -65
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +22 -9
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +34 -66
  33. package/lib/render/chrome.js +57 -22
  34. package/lib/render/commits.js +85 -0
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +29 -37
  37. package/lib/render/frame.js +29 -13
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +46 -1
  40. package/lib/{render.js → render/render.js} +6 -7
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +93 -36
  46. package/lib/session/branches.js +130 -144
  47. package/lib/session/change-actions.js +206 -124
  48. package/lib/session/commit.js +217 -62
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +109 -55
  51. package/lib/session/files-pane.js +145 -142
  52. package/lib/session/items.js +131 -34
  53. package/lib/session/lifecycle.js +82 -42
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +15 -3
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +58 -54
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +82 -11
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +29 -226
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +2 -2
  71. package/lib/deps/changes.js +0 -331
  72. package/lib/deps/items.js +0 -131
  73. package/lib/deps/manifest.js +0 -145
  74. package/lib/deps/merge.js +0 -120
  75. package/lib/deps/proposals.js +0 -164
  76. package/lib/deps/reports.js +0 -180
  77. package/lib/deps/rows.js +0 -177
  78. package/lib/deps/scan.js +0 -138
  79. package/lib/deps/usage.js +0 -68
  80. package/lib/deps/versions.js +0 -119
  81. package/lib/diff.js +0 -33
  82. package/lib/git/changes.js +0 -164
  83. package/lib/git/read.js +0 -253
  84. package/lib/git/run.js +0 -100
  85. package/lib/npm/commands.js +0 -100
  86. package/lib/remote/changes.js +0 -37
  87. package/lib/remote/headers.js +0 -36
  88. package/lib/remote/http.js +0 -122
  89. package/lib/remote/notes.js +0 -94
  90. package/lib/review/format.js +0 -434
  91. package/lib/review/storage.js +0 -150
  92. package/lib/review/store.js +0 -178
  93. package/lib/review/templates.js +0 -49
  94. package/lib/session/origins.js +0 -7
  95. package/lib/sys.js +0 -84
package/lib/github.js CHANGED
@@ -1,18 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { jsonParse } = require('metautil');
4
-
5
- const diff = require('./diff.js');
6
- const { parseDiff, itemsFromFiles } = diff;
7
3
  const { isPathInScope } = require('./files.js');
8
- const { foldDepItems } = require('./deps.js');
9
- const http = require('./remote/http.js');
10
- const { createTransport, isRecord, oneLine, isAbortError } = http;
11
- const { TRANSIENT_STATUS } = http;
12
- const { parseLinkNext } = require('./remote/headers.js');
13
- const { filterChangeFiles, changeFiles } = require('./remote/changes.js');
14
- const remoteNotes = require('./remote/notes.js');
15
- const { importedText, importedTodo, noteFromLocation } = remoteNotes;
4
+ const remote = require('./remote.js');
5
+ const { isRecord, isAbortError } = remote;
6
+ const { createClient, listUrl, parseLinkNext } = remote;
7
+ const { filterChangeFiles, changeFiles, loadChange } = remote;
8
+ const { importedText, importedTodo, noteFromLocation } = remote;
16
9
 
17
10
  const GITHUB_API = 'https://api.github.com';
18
11
  const API_VERSION = '2022-11-28';
@@ -20,7 +13,6 @@ const USER_AGENT = 'reslop';
20
13
  const JSON_ACCEPT = 'application/vnd.github+json';
21
14
  const DIFF_ACCEPT = 'application/vnd.github.diff';
22
15
  const PR_TODO_FILE = 'pull request';
23
- const LIST_PER_PAGE = 100;
24
16
  const IMPORT_HOST = 'github';
25
17
  const GRAPHQL_URL = `${GITHUB_API}/graphql`;
26
18
  const REVIEW_THREADS_QUERY =
@@ -41,17 +33,6 @@ const GITHUB_PR_RE = new RegExp(
41
33
  'i',
42
34
  );
43
35
 
44
- const githubError = (code, message) => {
45
- const error = new Error(message);
46
- error.code = code;
47
- return error;
48
- };
49
-
50
- const withStatus = (error, status) => {
51
- error.status = status;
52
- return error;
53
- };
54
-
55
36
  const asRecord = (value) => (isRecord(value) ? value : {});
56
37
 
57
38
  const parseGithubPrUrl = (text) => {
@@ -72,59 +53,7 @@ const githubToken = (env) => {
72
53
  return from.GITHUB_TOKEN || from.GH_TOKEN || '';
73
54
  };
74
55
 
75
- const parseApiMessage = (body) => {
76
- const text = `${body ?? ''}`.trim();
77
- if (!text) return '';
78
- const parsed = jsonParse(text);
79
- if (isRecord(parsed) && typeof parsed.message === 'string') {
80
- return parsed.message;
81
- }
82
- return oneLine(text);
83
- };
84
-
85
- const githubHttpError = (status, body, token) => {
86
- const message = parseApiMessage(body);
87
- if (status === 404) {
88
- const hint = token ? '' : ' (set GITHUB_TOKEN for private repositories)';
89
- const error = githubError(
90
- 'GITHUB_NOT_FOUND',
91
- `GitHub pull request not found${hint}`,
92
- );
93
- return withStatus(error, status);
94
- }
95
- if (status === 401) {
96
- const error = githubError('GITHUB_AUTH', 'GitHub authentication failed');
97
- return withStatus(error, status);
98
- }
99
- if (status === 403 && /rate limit/i.test(message)) {
100
- const error = githubError(
101
- 'GITHUB_RATE_LIMIT',
102
- 'GitHub API rate limit exceeded',
103
- );
104
- return withStatus(error, status);
105
- }
106
- let detail = `GitHub API HTTP ${status}`;
107
- if (message) detail = `GitHub API: ${oneLine(message)}`;
108
- return withStatus(githubError('GITHUB_HTTP', detail), status);
109
- };
110
-
111
- const wrapNetworkError = (error) => {
112
- const detail = oneLine(error && error.message);
113
- const suffix = detail ? `: ${detail}` : '';
114
- const wrapped = githubError(
115
- 'GITHUB_NETWORK',
116
- `GitHub request failed${suffix}`,
117
- );
118
- wrapped.cause = error;
119
- return wrapped;
120
- };
121
-
122
- const isRetryable = (error) => {
123
- if (error.code === 'GITHUB_NETWORK') return true;
124
- return TRANSIENT_STATUS.includes(error.status);
125
- };
126
-
127
- const githubHeaders = (token, accept) => {
56
+ const githubHeaders = (token, accept = JSON_ACCEPT) => {
128
57
  const headers = {
129
58
  Accept: accept,
130
59
  'User-Agent': USER_AGENT,
@@ -134,62 +63,17 @@ const githubHeaders = (token, accept) => {
134
63
  return { ...headers, Authorization: `Bearer ${token}` };
135
64
  };
136
65
 
137
- const parseGithubJson = (text, label) => {
138
- let data;
139
- try {
140
- data = JSON.parse(text);
141
- } catch {
142
- throw githubError('GITHUB_PARSE', `GitHub API: invalid ${label}`);
143
- }
144
- return data;
145
- };
146
-
147
- const transportOf = (options) =>
148
- createTransport({
149
- fetch: options.fetch,
150
- delay: options.delay,
151
- signal: options.signal,
152
- });
153
-
154
- const requestPolicy = (options) => {
155
- const token = options.token || '';
156
- return {
157
- retry: options.retry ?? {},
158
- isRetryable,
159
- wrapNetwork: wrapNetworkError,
160
- missingFetch: () => githubError('GITHUB_FETCH', 'fetch is not available'),
161
- toHttpError: (status, body) => githubHttpError(status, body, token),
162
- };
163
- };
164
-
165
- const requestGithub = async (url, options) => {
166
- const token = options.token || '';
167
- const headers = { ...githubHeaders(token, options.accept) };
168
- if (options.contentType) headers['Content-Type'] = options.contentType;
169
- return transportOf(options).request(url, {
170
- ...requestPolicy(options),
171
- method: options.method || 'GET',
172
- headers,
173
- body: options.body,
174
- });
175
- };
176
-
177
- const requestGithubList = async (url, options) => {
178
- const token = options.token || '';
179
- return transportOf(options).listPages(url, {
180
- ...requestPolicy(options),
181
- method: 'GET',
182
- headers: githubHeaders(token, JSON_ACCEPT),
183
- parsePage: (text) => {
184
- const data = parseGithubJson(text, 'list payload');
185
- if (!Array.isArray(data)) {
186
- throw githubError('GITHUB_PARSE', 'GitHub API: invalid list payload');
187
- }
188
- return data;
189
- },
190
- nextPage: (headers) => parseLinkNext(headers),
191
- });
192
- };
66
+ const client = createClient({
67
+ name: 'GitHub',
68
+ subject: 'pull request',
69
+ headers: githubHeaders,
70
+ nextPage: parseLinkNext,
71
+ });
72
+ const {
73
+ request: requestGithub,
74
+ list: requestGithubList,
75
+ parseJson: parseGithubJson,
76
+ } = client;
193
77
 
194
78
  const requestGraphql = async (query, variables, options) => {
195
79
  const payload = JSON.stringify({ query, variables });
@@ -200,11 +84,7 @@ const requestGraphql = async (query, variables, options) => {
200
84
  contentType: 'application/json',
201
85
  body: payload,
202
86
  });
203
- const data = parseGithubJson(page.body, 'GraphQL payload');
204
- if (!isRecord(data)) {
205
- throw githubError('GITHUB_PARSE', 'GitHub API: invalid GraphQL payload');
206
- }
207
- return data;
87
+ return parseGithubJson(page.body, 'GraphQL payload');
208
88
  };
209
89
 
210
90
  const prApiUrl = (pr) => {
@@ -213,12 +93,6 @@ const prApiUrl = (pr) => {
213
93
  return `${GITHUB_API}/repos/${owner}/${repo}/pulls/${pr.number}`;
214
94
  };
215
95
 
216
- const listUrl = (base) => {
217
- const url = new URL(base);
218
- url.searchParams.set('per_page', `${LIST_PER_PAGE}`);
219
- return url.toString();
220
- };
221
-
222
96
  const discussionUrls = (pr) => {
223
97
  const pull = prApiUrl(pr);
224
98
  const issue = pull.replace('/pulls/', '/issues/');
@@ -247,17 +121,6 @@ const readRepoName = (data, pr) => {
247
121
  return `${pr.owner}/${pr.repo}`;
248
122
  };
249
123
 
250
- const parsePullRequestPayload = (text) => {
251
- const data = parseGithubJson(text, 'pull request payload');
252
- if (!isRecord(data)) {
253
- throw githubError(
254
- 'GITHUB_PARSE',
255
- 'GitHub API: invalid pull request payload',
256
- );
257
- }
258
- return data;
259
- };
260
-
261
124
  const toChange = (pr, data, items) => {
262
125
  const number = data.number || pr.number;
263
126
  const repository = readRepoName(data, pr);
@@ -271,7 +134,7 @@ const toChange = (pr, data, items) => {
271
134
  const id = `github.com/${repository}/pull/${number}`;
272
135
  return {
273
136
  id,
274
- source: 'github-pr',
137
+ source: 'pr',
275
138
  title,
276
139
  author,
277
140
  files,
@@ -441,48 +304,16 @@ const loadDiscussion = async (pr, options) => {
441
304
  return { reviewComments, reviews, issueComments, resolvedById };
442
305
  };
443
306
 
444
- const loadImportedNotes = async (pr, items, paths, options) => {
445
- try {
446
- const discussion = await loadDiscussion(pr, options);
447
- return discussionToNotes(discussion, items, paths);
448
- } catch (error) {
449
- if (isAbortError(error, options.signal)) throw error;
450
- // ignore: discussion import is optional
451
- return { feedback: [], todos: [] };
452
- }
453
- };
454
-
455
- const loadPullRequest = async (pr, options = {}) => {
456
- const token = options.token ?? '';
457
- const fetchImpl = options.fetch ?? globalThis.fetch;
458
- const paths = options.paths ?? [];
459
- const retryOpts = options.retry ?? {};
460
- const url = prApiUrl(pr);
461
- const request = {
462
- fetch: fetchImpl,
463
- token,
464
- retry: retryOpts,
465
- delay: options.delay,
466
- signal: options.signal,
467
- };
468
- const jsonPage = await requestGithub(url, {
469
- ...request,
470
- accept: JSON_ACCEPT,
471
- });
472
- const data = parsePullRequestPayload(jsonPage.body);
473
- const diffPage = await requestGithub(url, {
474
- ...request,
475
- accept: DIFF_ACCEPT,
307
+ const loadPullRequest = (pr, options = {}) =>
308
+ loadChange(pr, options, {
309
+ client,
310
+ url: prApiUrl(pr),
311
+ diffAccept: DIFF_ACCEPT,
312
+ prefix: '#',
313
+ toChange,
314
+ loadDiscussion,
315
+ discussionToNotes,
476
316
  });
477
- const parsed = parseDiff(diffPage.body);
478
- const files = filterChangeFiles(parsed, paths);
479
- const parsedItems = itemsFromFiles(files, 'pr');
480
- const items = foldDepItems(parsedItems);
481
- const change = toChange(pr, data, items);
482
- const sourceLabel = `#${change.number}`;
483
- const imported = await loadImportedNotes(pr, items, paths, request);
484
- return { top: options.cwd, items, sourceLabel, change, imported };
485
- };
486
317
 
487
318
  module.exports = {
488
319
  parseGithubPrUrl,
package/lib/gitlab.js CHANGED
@@ -1,24 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const { jsonParse } = require('metautil');
4
-
5
- const diff = require('./diff.js');
6
- const { parseDiff, itemsFromFiles } = diff;
7
3
  const { isPathInScope } = require('./files.js');
8
- const { foldDepItems } = require('./deps.js');
9
- const http = require('./remote/http.js');
10
- const { createTransport, isRecord, oneLine, isAbortError } = http;
11
- const { TRANSIENT_STATUS } = http;
12
- const { parseNextPage } = require('./remote/headers.js');
13
- const { filterChangeFiles, changeFiles } = require('./remote/changes.js');
14
- const remoteNotes = require('./remote/notes.js');
15
- const { importedText, importedTodo, noteFromLocation } = remoteNotes;
4
+ const remote = require('./remote.js');
5
+ const { isRecord } = remote;
6
+ const { createClient, listUrl, parseNextPage } = remote;
7
+ const { filterChangeFiles, changeFiles, loadChange } = remote;
8
+ const { importedText, importedTodo, noteFromLocation } = remote;
16
9
 
17
10
  const USER_AGENT = 'reslop';
18
11
  const JSON_ACCEPT = 'application/json';
19
12
  const DIFF_ACCEPT = 'text/plain';
20
13
  const MR_TODO_FILE = 'merge request';
21
- const LIST_PER_PAGE = 100;
22
14
  const IMPORT_HOST = 'gitlab';
23
15
 
24
16
  const GITLAB_MR_RE = new RegExp(
@@ -30,17 +22,6 @@ const GITLAB_MR_RE = new RegExp(
30
22
  'i',
31
23
  );
32
24
 
33
- const gitlabError = (code, message) => {
34
- const error = new Error(message);
35
- error.code = code;
36
- return error;
37
- };
38
-
39
- const withStatus = (error, status) => {
40
- error.status = status;
41
- return error;
42
- };
43
-
44
25
  const parseGitlabMrUrl = (text) => {
45
26
  if (typeof text !== 'string' || !text) return null;
46
27
  const match = GITLAB_MR_RE.exec(text.trim());
@@ -61,67 +42,7 @@ const gitlabToken = (env) => {
61
42
  return from.GITLAB_TOKEN || from.GL_TOKEN || '';
62
43
  };
63
44
 
64
- const parseApiMessage = (body) => {
65
- const text = `${body ?? ''}`.trim();
66
- if (!text) return '';
67
- const parsed = jsonParse(text);
68
- if (isRecord(parsed)) {
69
- if (typeof parsed.message === 'string') return parsed.message;
70
- if (typeof parsed.error === 'string') return parsed.error;
71
- }
72
- return oneLine(text);
73
- };
74
-
75
- const gitlabHttpError = (status, body, token) => {
76
- const message = parseApiMessage(body);
77
- if (status === 404) {
78
- const hint = token ? '' : ' (set GITLAB_TOKEN for private repositories)';
79
- const error = gitlabError(
80
- 'GITLAB_NOT_FOUND',
81
- `GitLab merge request not found${hint}`,
82
- );
83
- return withStatus(error, status);
84
- }
85
- if (status === 401) {
86
- const error = gitlabError('GITLAB_AUTH', 'GitLab authentication failed');
87
- return withStatus(error, status);
88
- }
89
- if (status === 429) {
90
- const error = gitlabError(
91
- 'GITLAB_RATE_LIMIT',
92
- 'GitLab API rate limit exceeded',
93
- );
94
- return withStatus(error, status);
95
- }
96
- if (status === 403 && /rate limit/i.test(message)) {
97
- const error = gitlabError(
98
- 'GITLAB_RATE_LIMIT',
99
- 'GitLab API rate limit exceeded',
100
- );
101
- return withStatus(error, status);
102
- }
103
- let detail = `GitLab API HTTP ${status}`;
104
- if (message) detail = `GitLab API: ${oneLine(message)}`;
105
- return withStatus(gitlabError('GITLAB_HTTP', detail), status);
106
- };
107
-
108
- const wrapNetworkError = (error) => {
109
- const detail = oneLine(error && error.message);
110
- const suffix = detail ? `: ${detail}` : '';
111
- const wrapped = gitlabError(
112
- 'GITLAB_NETWORK',
113
- `GitLab request failed${suffix}`,
114
- );
115
- wrapped.cause = error;
116
- return wrapped;
117
- };
118
-
119
- const isRetryable = (error) => {
120
- if (error.code === 'GITLAB_NETWORK') return true;
121
- return TRANSIENT_STATUS.includes(error.status);
122
- };
123
-
124
- const gitlabHeaders = (token, accept) => {
45
+ const gitlabHeaders = (token, accept = JSON_ACCEPT) => {
125
46
  const headers = {
126
47
  Accept: accept,
127
48
  'User-Agent': USER_AGENT,
@@ -130,59 +51,13 @@ const gitlabHeaders = (token, accept) => {
130
51
  return { ...headers, 'PRIVATE-TOKEN': token };
131
52
  };
132
53
 
133
- const parseGitlabJson = (text, label) => {
134
- let data;
135
- try {
136
- data = JSON.parse(text);
137
- } catch {
138
- throw gitlabError('GITLAB_PARSE', `GitLab API: invalid ${label}`);
139
- }
140
- return data;
141
- };
142
-
143
- const transportOf = (options) =>
144
- createTransport({
145
- fetch: options.fetch,
146
- delay: options.delay,
147
- signal: options.signal,
148
- });
149
-
150
- const requestPolicy = (options) => {
151
- const token = options.token || '';
152
- return {
153
- retry: options.retry ?? {},
154
- isRetryable,
155
- wrapNetwork: wrapNetworkError,
156
- missingFetch: () => gitlabError('GITLAB_FETCH', 'fetch is not available'),
157
- toHttpError: (status, body) => gitlabHttpError(status, body, token),
158
- };
159
- };
160
-
161
- const requestGitlab = async (url, options) => {
162
- const token = options.token || '';
163
- return transportOf(options).request(url, {
164
- ...requestPolicy(options),
165
- method: options.method || 'GET',
166
- headers: gitlabHeaders(token, options.accept),
167
- });
168
- };
169
-
170
- const requestGitlabList = async (url, options) => {
171
- const token = options.token || '';
172
- return transportOf(options).listPages(url, {
173
- ...requestPolicy(options),
174
- method: 'GET',
175
- headers: gitlabHeaders(token, JSON_ACCEPT),
176
- parsePage: (text) => {
177
- const data = parseGitlabJson(text, 'list payload');
178
- if (!Array.isArray(data)) {
179
- throw gitlabError('GITLAB_PARSE', 'GitLab API: invalid list payload');
180
- }
181
- return data;
182
- },
183
- nextPage: parseNextPage,
184
- });
185
- };
54
+ const client = createClient({
55
+ name: 'GitLab',
56
+ subject: 'merge request',
57
+ headers: gitlabHeaders,
58
+ nextPage: parseNextPage,
59
+ });
60
+ const { list: requestGitlabList } = client;
186
61
 
187
62
  const mrApiUrl = (mr) => {
188
63
  const project = encodeURIComponent(mr.project);
@@ -190,12 +65,6 @@ const mrApiUrl = (mr) => {
190
65
  return `${base}/merge_requests/${mr.number}`;
191
66
  };
192
67
 
193
- const listUrl = (base) => {
194
- const url = new URL(base);
195
- url.searchParams.set('per_page', `${LIST_PER_PAGE}`);
196
- return url.toString();
197
- };
198
-
199
68
  const readUsername = (user) => {
200
69
  if (!isRecord(user)) return '';
201
70
  return user.username || '';
@@ -211,17 +80,6 @@ const readProject = (data, mr) => {
211
80
  return mr.project;
212
81
  };
213
82
 
214
- const parseMergeRequestPayload = (text) => {
215
- const data = parseGitlabJson(text, 'merge request payload');
216
- if (!isRecord(data)) {
217
- throw gitlabError(
218
- 'GITLAB_PARSE',
219
- 'GitLab API: invalid merge request payload',
220
- );
221
- }
222
- return data;
223
- };
224
-
225
83
  const toChange = (mr, data, items) => {
226
84
  const number = data.iid || mr.number;
227
85
  const repository = readProject(data, mr);
@@ -235,7 +93,7 @@ const toChange = (mr, data, items) => {
235
93
  const id = `${mr.host}/${repository}/merge_requests/${number}`;
236
94
  return {
237
95
  id,
238
- source: 'gitlab-mr',
96
+ source: 'mr',
239
97
  title,
240
98
  author,
241
99
  files,
@@ -322,47 +180,18 @@ const loadDiscussion = async (mr, options) => {
322
180
  return requestGitlabList(url, options);
323
181
  };
324
182
 
325
- const loadImportedNotes = async (mr, items, paths, options) => {
326
- try {
327
- const discussions = await loadDiscussion(mr, options);
328
- return discussionToNotes(discussions, items, paths);
329
- } catch (error) {
330
- if (isAbortError(error, options.signal)) throw error;
331
- // ignore: discussion import is optional
332
- return { feedback: [], todos: [] };
333
- }
334
- };
335
-
336
- const loadMergeRequest = async (mr, options = {}) => {
337
- const token = options.token ?? '';
338
- const fetchImpl = options.fetch ?? globalThis.fetch;
339
- const paths = options.paths ?? [];
340
- const retryOpts = options.retry ?? {};
183
+ const loadMergeRequest = (mr, options = {}) => {
341
184
  const url = mrApiUrl(mr);
342
- const request = {
343
- fetch: fetchImpl,
344
- token,
345
- retry: retryOpts,
346
- delay: options.delay,
347
- signal: options.signal,
348
- };
349
- const jsonPage = await requestGitlab(url, {
350
- ...request,
351
- accept: JSON_ACCEPT,
352
- });
353
- const data = parseMergeRequestPayload(jsonPage.body);
354
- const diffPage = await requestGitlab(`${url}/raw_diffs`, {
355
- ...request,
356
- accept: DIFF_ACCEPT,
185
+ return loadChange(mr, options, {
186
+ client,
187
+ url,
188
+ diffUrl: `${url}/raw_diffs`,
189
+ diffAccept: DIFF_ACCEPT,
190
+ prefix: '!',
191
+ toChange,
192
+ loadDiscussion,
193
+ discussionToNotes,
357
194
  });
358
- const parsed = parseDiff(diffPage.body);
359
- const files = filterChangeFiles(parsed, paths);
360
- const parsedItems = itemsFromFiles(files, 'pr');
361
- const items = foldDepItems(parsedItems);
362
- const change = toChange(mr, data, items);
363
- const sourceLabel = `!${change.number}`;
364
- const imported = await loadImportedNotes(mr, items, paths, request);
365
- return { top: options.cwd, items, sourceLabel, change, imported };
366
195
  };
367
196
 
368
197
  module.exports = {
@@ -143,8 +143,6 @@ const bashWordStyle = (word, nextCh) => {
143
143
  return 'plain';
144
144
  };
145
145
 
146
- const bashOpLen = (ch, next) => ('|&><'.includes(ch) && next === ch ? 2 : 1);
147
-
148
146
  const highlight = (source) => {
149
147
  const out = [];
150
148
  let i = 0;
@@ -205,7 +203,7 @@ const highlight = (source) => {
205
203
  }
206
204
 
207
205
  if ('|&;<>(){}'.includes(ch)) {
208
- const j = i + bashOpLen(ch, next);
206
+ const j = i + ('|&><'.includes(ch) && next === ch ? 2 : 1);
209
207
  emit(out, 'operator', source.slice(i, j));
210
208
  i = j;
211
209
  continue;
@@ -7,8 +7,6 @@ const CSS_NEXT_STYLE = {
7
7
  '{': 'tag',
8
8
  };
9
9
 
10
- const cssWordStyle = (next) => CSS_NEXT_STYLE[next] ?? 'plain';
11
-
12
10
  const highlight = (source) => {
13
11
  const out = [];
14
12
  let i = 0;
@@ -56,7 +54,7 @@ const highlight = (source) => {
56
54
  const word = source.slice(i, j);
57
55
  let k = j;
58
56
  while (k < n && /\s/.test(source[k])) k += 1;
59
- emit(out, cssWordStyle(source[k]), word);
57
+ emit(out, CSS_NEXT_STYLE[source[k]] ?? 'plain', word);
60
58
  i = j;
61
59
  continue;
62
60
  }
@@ -178,10 +178,6 @@ const TS_WORD_STYLE = {
178
178
  ...wordStyle(TYPES, 'type'),
179
179
  };
180
180
 
181
- const plainTail = (out) => tokensText(out).slice(-8);
182
-
183
- const styleExpected = ({ expectName }) => EXPECT_STYLE[expectName];
184
-
185
181
  const styleKeyword = ({ word, ts }) => {
186
182
  const table = ts ? TS_WORD_STYLE : WORD_STYLE;
187
183
  if (!Object.hasOwn(table, word)) return null;
@@ -199,12 +195,12 @@ const styleAssign = ({ after, source, k }) => {
199
195
  };
200
196
 
201
197
  const styleProperty = ({ out }) => {
202
- const prev = plainTail(out);
198
+ const prev = tokensText(out).slice(-8);
203
199
  return /\.$/.test(prev) ? 'property' : null;
204
200
  };
205
201
 
206
202
  const IDENT_RULES = [
207
- styleExpected,
203
+ ({ expectName }) => EXPECT_STYLE[expectName],
208
204
  styleKeyword,
209
205
  styleConstant,
210
206
  stylePascal,
@@ -496,9 +492,6 @@ const readPunctAt = (source, i) => {
496
492
  return { end: i + 1, tokens: [{ text, style: 'punct' }] };
497
493
  };
498
494
 
499
- const readDotToken = (source, i) =>
500
- readNumberAt(source, i) || readPunctAt(source, i);
501
-
502
495
  const readDecorator = (source, i) => {
503
496
  let j = i + 1;
504
497
  while (j < source.length && /[A-Za-z0-9_$]/.test(source[j])) j += 1;
@@ -534,9 +527,6 @@ const readJsxAt = (source, i, scan) => {
534
527
  return { end, tokens, expectName: null };
535
528
  };
536
529
 
537
- const readLessToken = (source, i, scan) =>
538
- readJsxAt(source, i, scan) || readOperatorAt(source, i);
539
-
540
530
  const readPlain = (source, i) => {
541
531
  const text = source[i];
542
532
  return { end: i + 1, tokens: [{ text, style: 'plain' }] };
@@ -544,8 +534,15 @@ const readPlain = (source, i) => {
544
534
 
545
535
  const CHAR_READER_GROUPS = [
546
536
  { chars: '/', reader: readSlashToken },
547
- { chars: '<', reader: readLessToken },
548
- { chars: '.', reader: readDotToken },
537
+ {
538
+ chars: '<',
539
+ reader: (source, i, scan) =>
540
+ readJsxAt(source, i, scan) || readOperatorAt(source, i),
541
+ },
542
+ {
543
+ chars: '.',
544
+ reader: (source, i) => readNumberAt(source, i) || readPunctAt(source, i),
545
+ },
549
546
  { chars: '`', reader: readTemplateAt },
550
547
  { chars: QUOTES, reader: readQuoted },
551
548
  { chars: '@', reader: readDecorator },