reslop 0.2.4 → 0.2.6

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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -11,7 +11,7 @@ const { listedCommits } = commit;
11
11
  const editor = require('../editor.js');
12
12
  const { Editor } = editor;
13
13
  const files = require('../files.js');
14
- const { itemPath, isTodoItem, listPath, isReadOnlyOrigin } = files;
14
+ const { itemPath, isTaskItem, listPath, isReadOnlyOrigin } = files;
15
15
  const diff = require('../diff/diff.js');
16
16
  const { blockAddText } = diff;
17
17
  const items = require('./items.js');
@@ -19,163 +19,28 @@ const { itemFeedKey, canEditCode } = items;
19
19
  const review = require('../review.js');
20
20
  const { setFeedback, setCode, checkLabel, rememberTemplate } = review;
21
21
  const templates = require('./templates.js');
22
- const { createTemplatePick } = templates;
23
- const todos = require('./todos.js');
24
- const { createTodoCompose } = todos;
22
+ const { TemplatePick } = templates;
23
+ const tasks = require('./tasks.js');
24
+ const { TaskPage } = tasks;
25
25
  const unit = require('./unit.js');
26
+ const clipboard = require('../clipboard.js');
26
27
 
27
28
  const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
28
- const COMPOSE_LEAVE = ['next', 'prev', 'todo'];
29
+ const COMPOSE_CLIP = ['ctrl-x', 'ctrl-v'];
30
+ const COMPOSE_LEAVE = ['next', 'prev', 'tasks'];
29
31
 
30
32
  const KIND = {
31
- feedback: {
32
- enter: 'save',
33
- tab: 'template',
34
- escape: 'save',
35
- move: 'note',
36
- command: null,
37
- view: 'note',
38
- },
39
- code: {
40
- enter: 'newline',
41
- tab: 'insert-tab',
42
- escape: 'save',
43
- move: 'code',
44
- command: null,
45
- view: 'code',
46
- },
47
- file: {
48
- enter: 'newline',
49
- tab: 'insert-tab',
50
- escape: 'save',
51
- move: 'code',
52
- command: null,
53
- view: 'code',
54
- },
55
- todo: {
56
- enter: 'edit-next',
57
- tab: 'template',
58
- escape: 'save',
59
- move: 'todo',
60
- command: null,
61
- view: 'todo',
62
- },
63
- commit: {
64
- enter: 'save',
65
- tab: 'template',
66
- escape: 'cancel',
67
- move: 'note',
68
- command: 'commit',
69
- view: 'note',
70
- },
71
- branch: {
72
- enter: 'save',
73
- tab: 'template',
74
- escape: 'cancel',
75
- move: 'note',
76
- command: 'branch',
77
- view: 'note',
78
- },
79
- npm: {
80
- enter: 'save',
81
- tab: 'insert-tab',
82
- escape: 'cancel',
83
- move: 'note',
84
- command: 'npm',
85
- view: 'note',
86
- },
33
+ feedback: { enter: 'save', tab: 'template', escape: 'save', move: 'note' },
34
+ code: { enter: 'newline', tab: 'insert-tab', escape: 'save', move: 'code' },
35
+ file: { enter: 'newline', tab: 'insert-tab', escape: 'save', move: 'code' },
36
+ tasks: { enter: 'edit-next', tab: 'template', escape: 'save', move: 'tasks' },
37
+ commit: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
38
+ branch: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
39
+ npm: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
87
40
  };
88
41
 
89
- const initialState = () => ({
90
- editor: null,
91
- composeKind: null,
92
- commitKind: null,
93
- commitBody: '',
94
- composeTodoId: null,
95
- composeBaseline: '',
96
- templateIndex: -1,
97
- templateFocus: false,
98
- blinkOn: true,
99
- fileDirty: false,
100
- fileEditRows: null,
101
- fileOpened: '',
102
- });
103
-
104
- const reset = (state) => {
105
- Object.assign(state, initialState());
106
- };
107
-
108
- const resetBlink = (api) => {
109
- api.state.blinkOn = true;
110
- api.restartBlink();
111
- };
112
-
113
- const tickBlink = (api) => {
114
- if (!api.state.composeKind) return;
115
- api.state.blinkOn = !api.state.blinkOn;
116
- api.ui.draw();
117
- };
118
-
119
- const viewSize = (api) => api.ui.lastSize ?? api.ui.getSize();
120
-
121
- const syncCodeScroll = (api) => {
122
- const kind = api.state.composeKind;
123
- if (kind !== 'code' && kind !== 'file') return;
124
- const ed = api.state.editor;
125
- if (!ed) return;
126
- const size = viewSize(api);
127
- const inner = codeInnerWidth(size.width, api.ui.layout);
128
- ed.reveal(inner);
129
- };
130
-
131
- const followFileEdit = (api) => {
132
- if (api.state.composeKind !== 'file') return;
133
- const text = api.state.editor ? api.state.editor.text : null;
134
- const rows = unit.linesFor(
135
- api.ui,
136
- api.nav.reviewPath,
137
- text,
138
- api.state.fileEditRows,
139
- );
140
- unit.followFileCursor(api.nav, api.state.editor, api.ui.lastFrame, rows);
141
- };
142
-
143
- const afterClose = (api) => {
144
- api.ui.clampIndex();
145
- api.ui.syncReviewPath();
146
- api.ui.syncFileCursor();
147
- };
148
-
149
- const openCompose = (api, kind, text, todoId) => {
150
- const { state, ui, nav } = api;
151
- ui.mode = 'compose';
152
- state.composeKind = kind;
153
- state.composeTodoId = todoId ?? null;
154
- state.composeBaseline = text;
155
- state.fileOpened = kind === 'file' ? text : '';
156
- state.editor = new Editor(text);
157
- state.templateIndex = -1;
158
- state.templateFocus = false;
159
- ui.status = '';
160
- nav.clearSelection();
161
- resetBlink(api);
162
- };
163
-
164
- const closeCompose = (api) => {
165
- const { state, ui } = api;
166
- ui.mode = 'review';
167
- state.editor = null;
168
- state.composeKind = null;
169
- state.commitKind = null;
170
- state.commitBody = '';
171
- state.composeTodoId = null;
172
- state.composeBaseline = '';
173
- state.fileOpened = '';
174
- state.templateIndex = -1;
175
- state.templateFocus = false;
176
- state.fileEditRows = null;
177
- afterClose(api);
178
- };
42
+ const COMMAND_KINDS = ['commit', 'branch', 'npm'];
43
+ const NOTE_VIEWS = ['feedback', 'commit', 'branch', 'npm'];
179
44
 
180
45
  const itemNote = (item, text) => {
181
46
  const hunk = item.hunk;
@@ -190,586 +55,606 @@ const itemNote = (item, text) => {
190
55
  };
191
56
  };
192
57
 
193
- const writesCodeFile = (api, item) => {
194
- if (!canEditCode(item)) return false;
195
- const caps = api.ui.capabilities;
196
- if (!caps || caps.changes !== true) return false;
197
- if (isReadOnlyOrigin(item.origin)) return false;
198
- const repo = api.ui.repo;
199
- return !!(repo && typeof repo.edit === 'function');
200
- };
201
-
202
- const commitFeedback = (api, text) => {
203
- const item = api.ui.current();
204
- if (!item || isTodoItem(item)) return;
205
- const notes = api.review.store;
206
- setFeedback(notes, itemFeedKey(item), itemNote(item, text));
58
+ const onEmptyLastLine = (editor) => {
59
+ if (!editor) return false;
60
+ const text = editor.text;
61
+ const lastBreak = text.lastIndexOf('\n');
62
+ if (editor.cursor < lastBreak + 1) return false;
63
+ return text.slice(lastBreak + 1).trim() === '';
207
64
  };
208
65
 
209
- const dropCodeNote = (api, item, text) => {
210
- const notes = api.review.store;
211
- if (!notes) return;
212
- const key = itemFeedKey(item);
213
- if (!notes.code.has(key)) return;
214
- setCode(notes, key, { ...itemNote(item, text), clear: true });
215
- };
66
+ const filledLines = (text) =>
67
+ text.split('\n').filter((line) => line.trim() !== '').length;
216
68
 
217
- const saveCodeNote = (api, item, text) => {
218
- const original = blockAddText(item.hunk, item.blockId);
219
- const notes = api.review.store;
220
- const key = itemFeedKey(item);
221
- const note = itemNote(item, text);
222
- if (text === original) {
223
- return void setCode(notes, key, { ...note, clear: true });
69
+ const blankTail = (text) => {
70
+ const lines = text.split('\n');
71
+ let count = 0;
72
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
73
+ if (lines[i].trim() !== '') break;
74
+ count += 1;
224
75
  }
225
- setCode(notes, key, note);
76
+ return count;
226
77
  };
227
78
 
228
- const saveCodeFile = (api, item, text) => {
229
- const original = blockAddText(item.hunk, item.blockId);
230
- if (text === original) {
231
- dropCodeNote(api, item, text);
232
- return true;
79
+ const COMPOSE_SPECIAL = {
80
+ backspace: (c) => c.editor.backspace(),
81
+ delete: (c) => c.editor.delete(),
82
+ left: (c) => c.editor.move(-1),
83
+ right: (c) => c.editor.move(1),
84
+ 'shift-left': (c) => c.editor.move(-1, true),
85
+ 'shift-right': (c) => c.editor.move(1, true),
86
+ 'ctrl-left': (c) => c.editor.moveWord(-1),
87
+ 'ctrl-right': (c) => c.editor.moveWord(1),
88
+ 'ctrl-shift-left': (c) => c.editor.moveWord(-1, true),
89
+ 'ctrl-shift-right': (c) => c.editor.moveWord(1, true),
90
+ up: (c) => c.moveLine(-1),
91
+ down: (c) => c.moveLine(1),
92
+ 'shift-up': (c) => c.moveLine(-1, true),
93
+ 'shift-down': (c) => c.moveLine(1, true),
94
+ home: (c) => c.home(),
95
+ end: (c) => c.end(),
96
+ 'shift-home': (c) => c.home(true),
97
+ 'shift-end': (c) => c.end(true),
98
+ pageUp: (c) => c.movePage(-1),
99
+ pageDown: (c) => c.movePage(1),
100
+ 'ctrl-a': (c) => c.editor.home(),
101
+ 'ctrl-e': (c) => c.editor.end(),
102
+ 'ctrl-c': (c) => c.copy(),
103
+ 'ctrl-x': (c) => c.cut(),
104
+ 'ctrl-v': (c) => c.paste(),
105
+ 'ctrl-z': (c) => c.editor.undoEdit(),
106
+ 'ctrl-y': (c) => c.editor.redoEdit(),
107
+ tab: (c) => c.tab(),
108
+ };
109
+
110
+ class Composer {
111
+ constructor(ui) {
112
+ this.ui = ui;
113
+ this.templates = new TemplatePick(this);
114
+ this.tasks = new TaskPage(this);
115
+ this.reset();
116
+ }
117
+
118
+ reset() {
119
+ this.fileDirty = false;
120
+ this.clear();
121
+ }
122
+
123
+ clear() {
124
+ this.editor = null;
125
+ this.composeKind = null;
126
+ this.commitKind = null;
127
+ this.commitBody = '';
128
+ this.composeTaskId = null;
129
+ this.composeBaseline = '';
130
+ this.templateIndex = -1;
131
+ this.templateFocus = false;
132
+ this.fileEditRows = null;
133
+ this.fileOpened = '';
134
+ }
135
+
136
+ get spec() {
137
+ return KIND[this.composeKind] ?? null;
138
+ }
139
+
140
+ get notes() {
141
+ return this.ui.review.store;
142
+ }
143
+
144
+ viewSize() {
145
+ return this.ui.lastSize ?? this.ui.getSize();
146
+ }
147
+
148
+ canChange() {
149
+ const caps = this.ui.capabilities;
150
+ return Boolean(caps && caps.changes === true);
151
+ }
152
+
153
+ syncCodeScroll() {
154
+ const kind = this.composeKind;
155
+ if (kind !== 'code' && kind !== 'file') return;
156
+ if (!this.editor) return;
157
+ const width = this.viewSize().width;
158
+ this.editor.reveal(codeInnerWidth(width, this.ui.layout));
159
+ }
160
+
161
+ followFileEdit() {
162
+ if (this.composeKind !== 'file') return;
163
+ const ui = this.ui;
164
+ const text = this.editor ? this.editor.text : null;
165
+ const rel = ui.nav.reviewPath;
166
+ const rows = unit.linesFor(ui, rel, text, this.fileEditRows);
167
+ unit.followFileCursor(ui.nav, this.editor, ui.lastFrame, rows);
168
+ }
169
+
170
+ openCompose(kind, text, taskId = null) {
171
+ this.ui.mode = 'compose';
172
+ this.composeKind = kind;
173
+ this.composeTaskId = taskId;
174
+ this.composeBaseline = text;
175
+ this.fileOpened = kind === 'file' ? text : '';
176
+ this.editor = new Editor(text);
177
+ this.templateIndex = -1;
178
+ this.templateFocus = false;
179
+ this.ui.status = '';
180
+ this.ui.nav.clearSelection();
181
+ }
182
+
183
+ closeCompose() {
184
+ const ui = this.ui;
185
+ ui.mode = 'review';
186
+ this.clear();
187
+ ui.clampIndex();
188
+ ui.syncReviewPath();
189
+ ui.syncFileCursor();
190
+ }
191
+
192
+ liveFileText() {
193
+ if (this.composeKind !== 'file' || !this.editor) return null;
194
+ return this.editor.text;
233
195
  }
234
- try {
235
- api.ui.repo.edit(api.ui.top, item, text);
236
- } catch (error) {
237
- api.ui.status = error.message;
238
- return false;
196
+
197
+ writesCodeFile(item) {
198
+ if (!canEditCode(item) || !this.canChange()) return false;
199
+ if (isReadOnlyOrigin(item.origin)) return false;
200
+ return Boolean(this.ui.repo.edit);
239
201
  }
240
- dropCodeNote(api, item, text);
241
- api.state.fileDirty = true;
242
- return true;
243
- };
244
202
 
245
- const commitCode = (api, text) => {
246
- const item = api.ui.current();
247
- if (!canEditCode(item)) return true;
248
- if (writesCodeFile(api, item)) return saveCodeFile(api, item, text);
249
- saveCodeNote(api, item, text);
250
- return true;
251
- };
203
+ commitFeedback(text) {
204
+ const item = this.ui.current();
205
+ if (!item || isTaskItem(item)) return;
206
+ setFeedback(this.notes, itemFeedKey(item), itemNote(item, text));
207
+ }
252
208
 
253
- const saveUnitFile = (api, text) => {
254
- const rel = api.nav.reviewPath;
255
- const repo = api.ui.repo;
256
- if (!rel || !repo || typeof repo.writeFile !== 'function') return true;
257
- const caps = api.ui.capabilities;
258
- if (!caps || caps.changes !== true) {
259
- api.ui.status = 'read only';
260
- return false;
209
+ dropCodeNote(item, text) {
210
+ const notes = this.notes;
211
+ if (!notes) return;
212
+ const key = itemFeedKey(item);
213
+ if (!notes.code.has(key)) return;
214
+ setCode(notes, key, { ...itemNote(item, text), clear: true });
261
215
  }
262
- try {
263
- repo.writeFile(api.ui.top, rel, text);
264
- } catch (error) {
265
- api.ui.status = error.message;
266
- return false;
216
+
217
+ commitCode(text) {
218
+ const ui = this.ui;
219
+ const item = ui.current();
220
+ if (!canEditCode(item)) return true;
221
+ const original = blockAddText(item.hunk, item.blockId);
222
+ if (!this.writesCodeFile(item)) {
223
+ const note = itemNote(item, text);
224
+ const clear = text === original;
225
+ setCode(this.notes, itemFeedKey(item), clear ? { ...note, clear } : note);
226
+ return true;
227
+ }
228
+ if (text !== original) {
229
+ try {
230
+ ui.repo.edit(ui.top, item, text);
231
+ } catch (error) {
232
+ ui.status = error.message;
233
+ return false;
234
+ }
235
+ this.fileDirty = true;
236
+ }
237
+ this.dropCodeNote(item, text);
238
+ return true;
267
239
  }
268
- if (typeof api.ui.ignoreWatch === 'function') api.ui.ignoreWatch();
269
- api.state.composeBaseline = text;
270
- api.state.fileDirty = true;
271
- return true;
272
- };
273
240
 
274
- const stageEditedFile = (api, rel) => {
275
- if (!rel) return false;
276
- const repo = api.ui.repo;
277
- if (!repo) return false;
278
- const caps = api.ui.capabilities;
279
- if (!caps || caps.changes !== true) return false;
280
- if (typeof repo.stagePath === 'function') {
241
+ saveUnitFile(text) {
242
+ const ui = this.ui;
243
+ const rel = ui.nav.reviewPath;
244
+ if (!rel || !ui.repo.writeFile) return true;
245
+ if (!this.canChange()) {
246
+ ui.status = 'read only';
247
+ return false;
248
+ }
281
249
  try {
282
- repo.stagePath(api.ui.top, rel);
250
+ ui.repo.writeFile(ui.top, rel, text);
283
251
  } catch (error) {
284
- api.ui.status = error.message;
252
+ ui.status = error.message;
285
253
  return false;
286
254
  }
287
- if (typeof api.ui.ignoreWatch === 'function') api.ui.ignoreWatch();
255
+ ui.ignoreWatch();
256
+ this.composeBaseline = text;
257
+ this.fileDirty = true;
288
258
  return true;
289
259
  }
290
- if (typeof repo.add !== 'function') return false;
291
- let staged = 0;
292
- for (const item of api.ui.items ?? []) {
293
- if (listPath(item) !== rel) continue;
294
- if (isReadOnlyOrigin(item.origin)) continue;
295
- if (item.origin === 'staged') continue;
296
- try {
297
- repo.add(api.ui.top, item);
298
- } catch (error) {
299
- api.ui.status = error.message;
300
- return staged > 0;
260
+
261
+ stageEditedFile(rel) {
262
+ const ui = this.ui;
263
+ const repo = ui.repo;
264
+ if (!rel || !this.canChange()) return false;
265
+ if (repo.stagePath) {
266
+ try {
267
+ repo.stagePath(ui.top, rel);
268
+ } catch (error) {
269
+ ui.status = error.message;
270
+ return false;
271
+ }
272
+ ui.ignoreWatch();
273
+ return true;
301
274
  }
302
- if (api.ui.collection && api.ui.collection.keepOrigin) {
303
- api.ui.collection.keepOrigin(item, 'staged');
275
+ if (!repo.add) return false;
276
+ let staged = 0;
277
+ for (const item of ui.items) {
278
+ if (listPath(item) !== rel) continue;
279
+ if (isReadOnlyOrigin(item.origin) || item.origin === 'staged') continue;
280
+ try {
281
+ repo.add(ui.top, item);
282
+ } catch (error) {
283
+ ui.status = error.message;
284
+ return staged > 0;
285
+ }
286
+ ui.collection.keepOrigin(item, 'staged');
287
+ staged += 1;
304
288
  }
305
- staged += 1;
289
+ if (staged) ui.ignoreWatch();
290
+ return staged > 0;
306
291
  }
307
- if (staged && typeof api.ui.ignoreWatch === 'function') {
308
- api.ui.ignoreWatch();
292
+
293
+ finishSave(reload, doneStatus) {
294
+ const ui = this.ui;
295
+ ui.flushReview();
296
+ this.closeCompose();
297
+ if (!reload) {
298
+ if (!ui.status) ui.status = doneStatus;
299
+ return;
300
+ }
301
+ ui.reloadAfterChange(() => {
302
+ if (!ui.status) ui.status = doneStatus;
303
+ });
309
304
  }
310
- return staged > 0;
311
- };
312
305
 
313
- const saveFileCompose = (api) => {
314
- const rel = api.nav.reviewPath;
315
- const opened = api.state.fileOpened;
316
- const text = api.state.editor ? api.state.editor.text : '';
317
- if (api.state.editor) {
318
- api.nav.unitLine = api.state.editor.linePos().line;
319
- }
320
- const result = saveUnitFile(api, text);
321
- if (result === false) return { kind: 'error' };
322
- const changed = text !== opened;
323
- const reload = api.state.fileDirty === true;
324
- if (changed) stageEditedFile(api, rel);
325
- api.ui.flushReview();
326
- closeCompose(api);
327
- const doneStatus = changed ? 'staged' : 'saved';
328
- if (!reload || typeof api.ui.reloadAfterChange !== 'function') {
329
- if (!api.ui.status) api.ui.status = doneStatus;
306
+ saveFileCompose() {
307
+ const rel = this.ui.nav.reviewPath;
308
+ const opened = this.fileOpened;
309
+ const text = this.editor ? this.editor.text : '';
310
+ if (this.editor) this.ui.nav.unitLine = this.editor.linePos().line;
311
+ if (!this.saveUnitFile(text)) return { kind: 'error' };
312
+ const changed = text !== opened;
313
+ const reload = this.fileDirty === true;
314
+ if (changed) this.stageEditedFile(rel);
315
+ this.finishSave(reload, changed ? 'staged' : 'saved');
330
316
  return { kind: 'saved' };
331
317
  }
332
- api.ui.reloadAfterChange(() => {
333
- if (!api.ui.status) api.ui.status = doneStatus;
334
- });
335
- return { kind: 'saved' };
336
- };
337
318
 
338
- const commitCompose = (api) => {
339
- const { state } = api;
340
- if (!state.composeKind || !state.editor) return true;
341
- const text = state.editor.text;
342
- if (state.composeKind === 'feedback') {
343
- if (api.review.store) commitFeedback(api, text);
344
- return true;
345
- }
346
- if (state.composeKind === 'todo') {
347
- api.todos.commitTodo(text);
319
+ commitCompose() {
320
+ const kind = this.composeKind;
321
+ if (!kind || !this.editor) return true;
322
+ const text = this.editor.text;
323
+ if (kind === 'feedback') {
324
+ if (this.notes) this.commitFeedback(text);
325
+ return true;
326
+ }
327
+ if (kind === 'tasks') {
328
+ this.tasks.commitTask(text);
329
+ return true;
330
+ }
331
+ if (kind === 'code') return this.commitCode(text);
332
+ if (kind === 'file') return this.saveUnitFile(text);
348
333
  return true;
349
334
  }
350
- if (state.composeKind === 'code') return commitCode(api, text);
351
- if (state.composeKind === 'file') return saveUnitFile(api, text);
352
- return true;
353
- };
354
335
 
355
- const saveCompose = (api, extra) => {
356
- const opts = extra ?? {};
357
- const spec = KIND[api.state.composeKind];
358
- if (spec && spec.command) return { kind: spec.command };
359
- const kind = api.state.composeKind;
360
- if (kind === 'file') return saveFileCompose(api);
361
- const item = api.ui.current ? api.ui.current() : null;
362
- const shouldStage =
363
- kind === 'code' &&
364
- writesCodeFile(api, item) &&
365
- item &&
366
- item.origin !== 'staged';
367
- const rel = itemPath(item);
368
- const baseline = api.state.composeBaseline;
369
- const result = commitCompose(api);
370
- if (result === false) return { kind: 'error' };
371
- const reload = api.state.fileDirty === true;
372
- if (kind === 'feedback') {
373
- const notes = api.review.store;
374
- const editor = api.state.editor;
375
- const next = editor ? editor.text : '';
376
- if (notes) rememberTemplate(notes, baseline, next);
377
- }
378
- if (shouldStage && reload) stageEditedFile(api, rel);
379
- api.ui.flushReview();
380
- closeCompose(api);
381
- const doneStatus = shouldStage && reload ? 'staged' : 'saved';
382
- if (reload && typeof api.ui.reloadAfterChange === 'function') {
383
- api.ui.reloadAfterChange(() => {
384
- if (!api.ui.status) api.ui.status = doneStatus;
385
- });
386
- } else {
387
- api.ui.status = doneStatus;
336
+ saveCompose(opts = {}) {
337
+ const ui = this.ui;
338
+ const kind = this.composeKind;
339
+ if (COMMAND_KINDS.includes(kind)) return { kind };
340
+ if (kind === 'file') return this.saveFileCompose();
341
+ const item = ui.current();
342
+ const shouldStage =
343
+ kind === 'code' && this.writesCodeFile(item) && item.origin !== 'staged';
344
+ const rel = itemPath(item);
345
+ const baseline = this.composeBaseline;
346
+ if (this.commitCompose() === false) return { kind: 'error' };
347
+ const reload = this.fileDirty === true;
348
+ if (kind === 'feedback' && this.notes) {
349
+ const next = this.editor ? this.editor.text : '';
350
+ rememberTemplate(this.notes, baseline, next);
351
+ }
352
+ const staging = shouldStage && reload;
353
+ if (staging) this.stageEditedFile(rel);
354
+ const doneStatus = staging ? 'staged' : 'saved';
355
+ if (reload) {
356
+ this.finishSave(true, doneStatus);
357
+ } else {
358
+ ui.flushReview();
359
+ this.closeCompose();
360
+ ui.status = doneStatus;
361
+ }
362
+ if (opts.editNext === true) this.tasks.editNextTask();
363
+ return { kind: 'saved' };
388
364
  }
389
- if (opts.editNext === true) api.todos.editNextTodo();
390
- return { kind: 'saved' };
391
- };
392
365
 
393
- const autosave = (api) => {
394
- const { state } = api;
395
- const spec = KIND[state.composeKind];
396
- const text = state.editor ? state.editor.text : '';
397
- if (state.composeKind === 'file' && state.editor) {
398
- if (text === state.composeBaseline) return;
399
- return void saveUnitFile(api, text);
400
- }
401
- const editingCode = state.composeKind === 'code';
402
- const composing = state.composeKind !== null;
403
- const reviewKind = composing && (!spec || !spec.command);
404
- const liveItem = api.ui.current ? api.ui.current() : null;
405
- const fileEdit = editingCode && writesCodeFile(api, liveItem);
406
- if (reviewKind && !fileEdit && (editingCode || text.trim())) {
407
- commitCompose(api);
408
- }
409
- const notes = api.review.store;
410
- if (notes && notes.dirty) api.ui.flushReview();
411
- };
412
-
413
- const composeView = (api) => {
414
- const { state } = api;
415
- if (!state.composeKind || !state.editor) return null;
416
- const spec = KIND[state.composeKind];
417
- if (!spec || spec.view !== 'note') return null;
418
- const kind = state.composeKind;
419
- const text = state.editor.text;
420
- const cursor = state.editor.cursor;
421
- return { kind, text, cursor, commitKind: state.commitKind };
422
- };
423
-
424
- const liveCodeText = (api) => {
425
- const { state } = api;
426
- if (state.composeKind === 'file' && state.editor) return state.editor.text;
427
- const item = api.ui.current();
428
- if (!canEditCode(item)) return null;
429
- if (state.composeKind === 'code' && state.editor) return state.editor.text;
430
- if (writesCodeFile(api, item)) return null;
431
- const notes = api.review.store;
432
- if (!notes) return null;
433
- const note = notes.code.get(itemFeedKey(item));
434
- if (!note) return null;
435
- return note.text;
436
- };
437
-
438
- const codeOverlayView = (api) => {
439
- const text = liveCodeText(api);
440
- if (text === null) return null;
441
- const editing =
442
- api.state.composeKind === 'code' || api.state.composeKind === 'file';
443
- const editor = api.state.editor;
444
- const cursor = editing && editor ? editor.cursor : null;
445
- const picked = editing && editor ? editor.scrollCol : 0;
446
- const scrollCol = picked ?? 0;
447
- return { text, keepEmpty: editing, cursor, scrollCol };
448
- };
449
-
450
- const idleNoteText = (api) => {
451
- const notes = api.review.store;
452
- if (!notes) return '';
453
- if (api.state.composeKind) return '';
454
- const item = api.ui.current();
455
- if (!item || isTodoItem(item)) return '';
456
- const note = notes.feedback.get(itemFeedKey(item));
457
- if (!note || !note.text.trim()) return '';
458
- return checkLabel(note.text, note.done);
459
- };
366
+ autosave() {
367
+ const kind = this.composeKind;
368
+ const text = this.editor ? this.editor.text : '';
369
+ if (kind === 'file' && this.editor) {
370
+ if (text === this.composeBaseline) return;
371
+ return void this.saveUnitFile(text);
372
+ }
373
+ const editingCode = kind === 'code';
374
+ const reviewKind = kind !== null && !COMMAND_KINDS.includes(kind);
375
+ const fileEdit = editingCode && this.writesCodeFile(this.ui.current());
376
+ if (reviewKind && !fileEdit && (editingCode || text.trim())) {
377
+ this.commitCompose();
378
+ }
379
+ if (this.notes && this.notes.dirty) this.ui.flushReview();
380
+ }
381
+
382
+ composeView() {
383
+ const { composeKind: kind, editor } = this;
384
+ if (!editor || !NOTE_VIEWS.includes(kind)) return null;
385
+ const { text, cursor, scrollCol, anchor } = editor;
386
+ return {
387
+ kind,
388
+ text,
389
+ cursor,
390
+ scrollCol,
391
+ anchor,
392
+ commitKind: this.commitKind,
393
+ };
394
+ }
395
+
396
+ liveCodeText() {
397
+ const kind = this.composeKind;
398
+ if (kind === 'file' && this.editor) return this.editor.text;
399
+ const item = this.ui.current();
400
+ if (!canEditCode(item)) return null;
401
+ if (kind === 'code' && this.editor) return this.editor.text;
402
+ if (this.writesCodeFile(item) || !this.notes) return null;
403
+ const note = this.notes.code.get(itemFeedKey(item));
404
+ return note ? note.text : null;
405
+ }
406
+
407
+ codeOverlayView() {
408
+ const text = this.liveCodeText();
409
+ if (text === null) return null;
410
+ const editing = this.composeKind === 'code' || this.composeKind === 'file';
411
+ const editor = editing ? this.editor : null;
412
+ return {
413
+ text,
414
+ keepEmpty: editing,
415
+ cursor: editor?.cursor ?? null,
416
+ scrollCol: editor?.scrollCol ?? 0,
417
+ anchor: editor?.anchor ?? null,
418
+ };
419
+ }
420
+
421
+ idleNoteText() {
422
+ const notes = this.notes;
423
+ if (!notes || this.composeKind) return '';
424
+ const item = this.ui.current();
425
+ if (!item || isTaskItem(item)) return '';
426
+ const note = notes.feedback.get(itemFeedKey(item));
427
+ if (!note || !note.text.trim()) return '';
428
+ return checkLabel(note.text, note.done);
429
+ }
430
+
431
+ tab() {
432
+ if (this.spec && this.spec.tab === 'insert-tab') {
433
+ this.editor.insert('\t');
434
+ return null;
435
+ }
436
+ return this.templates.applyTemplate();
437
+ }
438
+
439
+ moveTemplateLine(delta, shown, width) {
440
+ const pick = this.templates;
441
+ if (this.templateFocus) {
442
+ const next = Math.max(
443
+ 0,
444
+ Math.min(this.templateIndex + delta, shown.length - 1),
445
+ );
446
+ if (next === this.templateIndex) this.templateFocus = false;
447
+ else pick.focusTemplate(next, false);
448
+ return true;
449
+ }
450
+ const pos = cursorInWrap(this.editor.text, this.editor.cursor, width);
451
+ const last = wrapDoc(this.editor.text, width).length - 1;
452
+ if (delta < 0 && pos.row === 0) {
453
+ pick.focusTemplate(shown.length - 1, false);
454
+ return true;
455
+ }
456
+ if (delta > 0 && pos.row === last) {
457
+ pick.focusTemplate(0, false);
458
+ return true;
459
+ }
460
+ return false;
461
+ }
460
462
 
461
- const composeTab = (api) => {
462
- const spec = KIND[api.state.composeKind];
463
- if (spec && spec.tab === 'insert-tab') {
464
- api.state.editor.insert('\t');
465
- return null;
463
+ noteLineWidth() {
464
+ const width = this.viewSize().width;
465
+ const fullCommit =
466
+ this.composeKind === 'commit' && this.ui.commits.commitView === 'full';
467
+ if (!fullCommit) return noteInnerWidth(width);
468
+ const inPlace = this.commitKind === 'amend' || this.commitKind === 'reword';
469
+ const list = listedCommits(this.ui.commits.commits);
470
+ const entry = (inPlace && list[this.ui.nav.commitCursor]) || null;
471
+ return commitMessageWidth(entry, width);
466
472
  }
467
- return api.templates.applyTemplate();
468
- };
469
473
 
470
- const moveTemplateLine = (api, delta, shown, width) => {
471
- const { state } = api;
472
- if (state.templateFocus) {
473
- let next = state.templateIndex + delta;
474
- if (next < 0) next = 0;
475
- if (next >= shown.length) next = shown.length - 1;
476
- if (next === state.templateIndex) {
477
- state.templateFocus = false;
478
- return true;
474
+ moveLine(delta, extend) {
475
+ const move = this.spec ? this.spec.move : 'note';
476
+ if (move === 'tasks') {
477
+ const inner = todoBodyWidth(this.viewSize().width);
478
+ return void this.tasks.moveTaskCompose(delta, inner, extend);
479
479
  }
480
- api.templates.focusTemplate(next, false);
481
- return true;
480
+ if (move === 'code') {
481
+ this.editor.moveLine(delta, undefined, extend);
482
+ return void this.syncCodeScroll();
483
+ }
484
+ const width = this.noteLineWidth();
485
+ const shown = this.templates.shownTemplates();
486
+ const feedback = this.composeKind === 'feedback';
487
+ if (shown.length && feedback && !extend) {
488
+ if (this.moveTemplateLine(delta, shown, width)) return;
489
+ }
490
+ this.editor.moveLine(delta, width, extend);
482
491
  }
483
- const pos = cursorInWrap(state.editor.text, state.editor.cursor, width);
484
- const last = wrapDoc(state.editor.text, width).length - 1;
485
- if (delta < 0 && pos.row === 0) {
486
- api.templates.focusTemplate(shown.length - 1, false);
487
- return true;
492
+
493
+ movePage(dir) {
494
+ const frame = this.ui.lastFrame;
495
+ const fallback = Math.max(1, (this.viewSize().height ?? 24) - 4);
496
+ const step = Math.max(1, frame?.bodyH ?? fallback);
497
+ this.moveLine(step * dir);
488
498
  }
489
- if (delta > 0 && pos.row === last) {
490
- api.templates.focusTemplate(0, false);
499
+
500
+ jumpTemplates(toEnd) {
501
+ if (!this.templateFocus) return false;
502
+ const shown = this.templates.shownTemplates();
503
+ if (!shown.length) return false;
504
+ this.templates.focusTemplate(toEnd ? shown.length - 1 : 0, false);
491
505
  return true;
492
506
  }
493
- return false;
494
- };
495
507
 
496
- const moveNoteLine = (api, delta) => {
497
- const size = viewSize(api);
498
- const width = noteInnerWidth(size.width);
499
- const shown = api.templates.shownTemplates();
500
- const feedback = api.state.composeKind === 'feedback';
501
- if (shown.length && feedback) {
502
- if (moveTemplateLine(api, delta, shown, width)) return;
508
+ home(extend) {
509
+ if (!extend && this.jumpTemplates(false)) return;
510
+ this.editor.home(extend);
503
511
  }
504
- api.state.editor.moveLine(delta, width);
505
- };
506
-
507
- const fullCommitEdit = (api) =>
508
- api.state.composeKind === 'commit' && api.ui.commits.commitView === 'full';
509
-
510
- const editedCommitEntry = (api) => {
511
- const kind = api.state.commitKind;
512
- if (kind !== 'amend' && kind !== 'reword') return null;
513
- const list = listedCommits(api.ui.commits.commits);
514
- return list[api.nav.commitCursor] ?? null;
515
- };
516
512
 
517
- const moveCommitLine = (api, delta) => {
518
- const size = viewSize(api);
519
- const width = commitMessageWidth(editedCommitEntry(api), size.width);
520
- api.state.editor.moveLine(delta, width);
521
- };
522
-
523
- const moveComposeLine = (api, delta) => {
524
- const spec = KIND[api.state.composeKind];
525
- const move = spec ? spec.move : 'note';
526
- if (move === 'todo') {
527
- const size = viewSize(api);
528
- const inner = todoBodyWidth(size.width);
529
- return void api.todos.moveTodoCompose(delta, inner);
530
- }
531
- if (move === 'code') {
532
- api.state.editor.moveLine(delta);
533
- return void syncCodeScroll(api);
513
+ end(extend) {
514
+ if (!extend && this.jumpTemplates(true)) return;
515
+ this.editor.end(extend);
534
516
  }
535
- if (fullCommitEdit(api)) return void moveCommitLine(api, delta);
536
- moveNoteLine(api, delta);
537
- };
538
-
539
- const composePageStep = (api) => {
540
- const frame = api.ui.lastFrame ?? null;
541
- const size = viewSize(api);
542
- const fallback = Math.max(1, (size.height ?? 24) - 4);
543
- return Math.max(1, frame?.bodyH ?? fallback);
544
- };
545
-
546
- const jumpComposeTemplates = (api, toEnd) => {
547
- if (!api.state.templateFocus) return false;
548
- const shown = api.templates.shownTemplates();
549
- if (!shown.length) return false;
550
- const index = toEnd ? shown.length - 1 : 0;
551
- api.templates.focusTemplate(index, false);
552
- return true;
553
- };
554
-
555
- const composeHome = (api) => {
556
- if (jumpComposeTemplates(api, false)) return;
557
- api.state.editor.home();
558
- };
559
517
 
560
- const composeEnd = (api) => {
561
- if (jumpComposeTemplates(api, true)) return;
562
- api.state.editor.end();
563
- };
564
-
565
- const moveComposePage = (api, dir) => {
566
- const step = composePageStep(api) * dir;
567
- moveComposeLine(api, step);
568
- };
569
-
570
- const onFeedback = (api) => {
571
- if (api.nav.pane === 'files') {
572
- api.ui.status = 'not a diff block';
573
- return;
518
+ onFeedback() {
519
+ const ui = this.ui;
520
+ const item = ui.current();
521
+ if (ui.nav.pane === 'files' || !item || isTaskItem(item)) {
522
+ ui.status = 'not a diff block';
523
+ return;
524
+ }
525
+ const prev = this.notes.feedback.get(itemFeedKey(item));
526
+ this.openCompose('feedback', prev ? prev.text : '');
574
527
  }
575
- const item = api.ui.current();
576
- if (!item || isTodoItem(item)) {
577
- api.ui.status = 'not a diff block';
578
- return;
528
+
529
+ onUnitEdit() {
530
+ const ui = this.ui;
531
+ if (!ui.nav.reviewPath) {
532
+ ui.status = 'not a diff block';
533
+ return;
534
+ }
535
+ if (!this.canChange()) {
536
+ ui.status = 'read only';
537
+ return;
538
+ }
539
+ const at = unit.editCursor(ui);
540
+ this.openCompose('file', at.text);
541
+ this.fileEditRows = at.rows ?? null;
542
+ this.editor.cursor = Math.min(at.cursor, at.text.length);
543
+ this.syncCodeScroll();
544
+ this.followFileEdit();
545
+ }
546
+
547
+ onCode() {
548
+ const ui = this.ui;
549
+ if (ui.nav.pane === 'unit') return void this.onUnitEdit();
550
+ const item = ui.current();
551
+ if (ui.nav.pane === 'files' || !canEditCode(item)) {
552
+ ui.status = 'not a diff block';
553
+ return;
554
+ }
555
+ const original = blockAddText(item.hunk, item.blockId);
556
+ const writes = this.writesCodeFile(item);
557
+ const prev = writes ? null : this.notes.code.get(itemFeedKey(item));
558
+ this.openCompose('code', prev ? prev.text : original);
559
+ this.syncCodeScroll();
579
560
  }
580
- const notes = api.review.store;
581
- const prev = notes.feedback.get(itemFeedKey(item));
582
- const text = prev ? prev.text : '';
583
- openCompose(api, 'feedback', text);
584
- };
585
561
 
586
- const onUnitEdit = (api) => {
587
- const rel = api.nav.reviewPath;
588
- if (!rel) {
589
- api.ui.status = 'not a diff block';
590
- return;
591
- }
592
- const caps = api.ui.capabilities;
593
- if (!caps || caps.changes !== true) {
594
- api.ui.status = 'read only';
595
- return;
596
- }
597
- const at = unit.editCursor(api.ui);
598
- openCompose(api, 'file', at.text);
599
- api.state.fileEditRows = at.rows ?? null;
600
- api.state.editor.cursor = Math.min(at.cursor, at.text.length);
601
- syncCodeScroll(api);
602
- followFileEdit(api);
603
- };
562
+ insertNewline() {
563
+ this.editor.insert('\n');
564
+ this.followFileEdit();
565
+ return null;
566
+ }
604
567
 
605
- const onCode = (api) => {
606
- if (api.nav.pane === 'unit') return void onUnitEdit(api);
607
- if (api.nav.pane === 'files') {
608
- api.ui.status = 'not a diff block';
609
- return;
610
- }
611
- const item = api.ui.current();
612
- if (!canEditCode(item)) {
613
- api.ui.status = 'not a diff block';
614
- return;
615
- }
616
- const original = blockAddText(item.hunk, item.blockId);
617
- const notes = api.review.store;
618
- const prev = writesCodeFile(api, item)
619
- ? null
620
- : notes.code.get(itemFeedKey(item));
621
- const text = prev ? prev.text : original;
622
- openCompose(api, 'code', text);
623
- syncCodeScroll(api);
624
- };
568
+ enter() {
569
+ const kind = this.composeKind;
570
+ if (this.templateFocus && kind === 'feedback') {
571
+ return this.templates.selectTemplate(this.templateIndex);
572
+ }
573
+ const spec = this.spec;
574
+ if (spec && spec.enter === 'newline') return this.insertNewline();
575
+ if (kind === 'commit' && this.ui.commits.commitView === 'full') {
576
+ const text = this.editor ? this.editor.text : '';
577
+ const shaped = filledLines(text) >= 2 || blankTail(text) >= 2;
578
+ if (onEmptyLastLine(this.editor) && shaped) return this.saveCompose();
579
+ return this.insertNewline();
580
+ }
581
+ if (spec && spec.enter === 'edit-next') {
582
+ return this.saveCompose({ editNext: true });
583
+ }
584
+ return this.saveCompose();
585
+ }
625
586
 
626
- const insertNewline = (api) => {
627
- api.state.editor.insert('\n');
628
- resetBlink(api);
629
- followFileEdit(api);
630
- return null;
631
- };
587
+ leave(key) {
588
+ if (this.spec && this.spec.escape === 'cancel' && key === 'escape') {
589
+ this.closeCompose();
590
+ this.ui.status = '';
591
+ return null;
592
+ }
593
+ return this.saveCompose();
594
+ }
632
595
 
633
- const onEmptyLastLine = (editor) => {
634
- if (!editor) return false;
635
- const text = editor.text;
636
- const lastBreak = text.lastIndexOf('\n');
637
- if (editor.cursor < lastBreak + 1) return false;
638
- return text.slice(lastBreak + 1).trim() === '';
639
- };
596
+ copy() {
597
+ const text = this.editor.selectedText();
598
+ if (!text) return;
599
+ const ok = clipboard.copyText(text, this.ui.stdout);
600
+ this.ui.status = ok ? 'copied' : 'copy failed';
601
+ }
640
602
 
641
- const enterCompose = (api) => {
642
- const { state } = api;
643
- if (state.templateFocus && state.composeKind === 'feedback') {
644
- return api.templates.selectTemplate(state.templateIndex);
603
+ cut() {
604
+ if (!this.editor.hasSelect()) return;
605
+ this.copy();
606
+ this.editor.removeSpan();
645
607
  }
646
- const spec = KIND[state.composeKind];
647
- if (spec && spec.enter === 'newline') return insertNewline(api);
648
- if (fullCommitEdit(api)) {
649
- if (onEmptyLastLine(state.editor)) return saveCompose(api);
650
- return insertNewline(api);
608
+
609
+ paste() {
610
+ const text = clipboard.pasteText();
611
+ if (text) this.editor.insert(text);
651
612
  }
652
- if (spec && spec.enter === 'edit-next') {
653
- return saveCompose(api, { editNext: true });
613
+
614
+ afterEdit() {
615
+ this.followFileEdit();
616
+ this.syncCodeScroll();
654
617
  }
655
- return saveCompose(api);
656
- };
657
618
 
658
- const leaveCompose = (api, key) => {
659
- const spec = KIND[api.state.composeKind];
660
- if (spec && spec.escape === 'cancel' && key === 'escape') {
661
- closeCompose(api);
662
- api.ui.status = '';
619
+ handleKey(key) {
620
+ if (key === 'enter') {
621
+ const result = this.enter();
622
+ this.syncCodeScroll();
623
+ return result;
624
+ }
625
+ if (key === 'escape' || key === 'ctrl-s') return this.leave(key);
626
+ const special = COMPOSE_SPECIAL[key];
627
+ if (special) {
628
+ const result = special(this);
629
+ const edits = COMPOSE_EDITS.includes(key) || COMPOSE_CLIP.includes(key);
630
+ if (edits) this.templates.clearTemplatePick();
631
+ this.afterEdit();
632
+ return result ?? null;
633
+ }
634
+ if (!key.length || key.startsWith('ctrl-') || key.startsWith('alt-')) {
635
+ return null;
636
+ }
637
+ if (key.charCodeAt(0) >= 32) {
638
+ this.templates.clearTemplatePick();
639
+ this.editor.insert(key);
640
+ this.afterEdit();
641
+ }
663
642
  return null;
664
643
  }
665
- return saveCompose(api);
666
- };
667
644
 
668
- const COMPOSE_SPECIAL = {
669
- backspace: (api) => api.state.editor.backspace(),
670
- delete: (api) => api.state.editor.delete(),
671
- left: (api) => api.state.editor.move(-1),
672
- right: (api) => api.state.editor.move(1),
673
- 'ctrl-left': (api) => api.state.editor.moveWord(-1),
674
- 'ctrl-right': (api) => api.state.editor.moveWord(1),
675
- up: (api) => moveComposeLine(api, -1),
676
- down: (api) => moveComposeLine(api, 1),
677
- home: (api) => composeHome(api),
678
- end: (api) => composeEnd(api),
679
- pageUp: (api) => moveComposePage(api, -1),
680
- pageDown: (api) => moveComposePage(api, 1),
681
- 'ctrl-a': (api) => api.state.editor.home(),
682
- 'ctrl-e': (api) => api.state.editor.end(),
683
- 'ctrl-z': (api) => api.state.editor.undoEdit(),
684
- 'ctrl-y': (api) => api.state.editor.redoEdit(),
685
- tab: (api) => composeTab(api),
686
- };
687
-
688
- const handleKey = (api, key) => {
689
- if (key === 'enter') {
690
- const result = enterCompose(api);
691
- syncCodeScroll(api);
692
- return result;
693
- }
694
- if (key === 'escape' || key === 'ctrl-s') return leaveCompose(api, key);
695
- const special = COMPOSE_SPECIAL[key];
696
- if (special) {
697
- const result = special(api);
698
- if (COMPOSE_EDITS.includes(key)) api.templates.clearTemplatePick();
699
- resetBlink(api);
700
- followFileEdit(api);
701
- syncCodeScroll(api);
702
- return result ?? null;
703
- }
704
- if (!key.length) return null;
705
- if (key.startsWith('ctrl-') || key.startsWith('alt-')) return null;
706
- if (key.charCodeAt(0) >= 32) {
707
- api.templates.clearTemplatePick();
708
- api.state.editor.insert(key);
709
- resetBlink(api);
710
- followFileEdit(api);
711
- syncCodeScroll(api);
712
- }
713
- return null;
714
- };
715
-
716
- const applyDiskText = (api, text) => {
717
- if (api.state.composeKind !== 'file' || !api.state.editor) return;
718
- if (text === api.state.editor.text) return;
719
- const cursor = api.state.editor.cursor;
720
- api.state.editor.text = text;
721
- api.state.editor.cursor = Math.min(cursor, text.length);
722
- api.state.composeBaseline = text;
723
- api.state.fileEditRows = unit.linesFor(api.ui, api.nav.reviewPath, null);
724
- syncCodeScroll(api);
725
- followFileEdit(api);
726
- };
727
-
728
- const composerMethods = (api) => ({
729
- reset: () => reset(api.state),
730
- openCompose: (kind, text, todoId) => openCompose(api, kind, text, todoId),
731
- closeCompose: () => closeCompose(api),
732
- commitCompose: () => commitCompose(api),
733
- saveCompose: (opts) => saveCompose(api, opts),
734
- autosave: () => autosave(api),
735
- composeView: () => composeView(api),
736
- codeOverlayView: () => codeOverlayView(api),
737
- idleNoteText: () => idleNoteText(api),
738
- resetBlink: () => resetBlink(api),
739
- tickBlink: () => tickBlink(api),
740
- onFeedback: () => onFeedback(api),
741
- onCode: () => onCode(api),
742
- handleKey: (key) => handleKey(api, key),
743
- liveFileText: () => {
744
- if (api.state.composeKind !== 'file' || !api.state.editor) return null;
745
- return api.state.editor.text;
746
- },
747
- fileEditRows: () => api.state.fileEditRows,
748
- applyDiskText: (text) => applyDiskText(api, text),
749
- });
750
-
751
- const createComposer = ({ nav, review, ui, restartBlink }) => {
752
- const state = initialState();
753
- const api = { nav, review, ui, restartBlink, state };
754
- api.open = (kind, text, todoId) => openCompose(api, kind, text, todoId);
755
- api.close = () => closeCompose(api);
756
- api.commit = () => commitCompose(api);
757
- api.save = (opts) => saveCompose(api, opts);
758
- api.resetBlink = () => resetBlink(api);
759
- api.templates = createTemplatePick(api);
760
- api.todos = createTodoCompose(api);
761
- const composer = {
762
- state,
763
- ...composerMethods(api),
764
- ...api.todos,
765
- ...api.templates,
766
- };
767
- return composer;
768
- };
645
+ applyDiskText(text) {
646
+ const editor = this.editor;
647
+ if (this.composeKind !== 'file' || !editor) return;
648
+ if (text === editor.text) return;
649
+ const cursor = editor.cursor;
650
+ editor.text = text;
651
+ editor.cursor = Math.min(cursor, text.length);
652
+ editor.anchor = null;
653
+ this.composeBaseline = text;
654
+ this.fileEditRows = unit.linesFor(this.ui, this.ui.nav.reviewPath, null);
655
+ this.syncCodeScroll();
656
+ this.followFileEdit();
657
+ }
658
+ }
769
659
 
770
- module.exports = {
771
- COMPOSE_EDITS,
772
- COMPOSE_LEAVE,
773
- KIND,
774
- createComposer,
775
- };
660
+ module.exports = { COMPOSE_LEAVE, Composer };