@adecore/editor 0.0.1 → 0.17.0-beta.2

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 (107) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +57 -0
  3. package/dist/attribution.d.ts +28 -0
  4. package/dist/attribution.js +55 -0
  5. package/dist/code-block.d.ts +2 -0
  6. package/dist/code-block.js +89 -0
  7. package/dist/column-selection.d.ts +9 -0
  8. package/dist/column-selection.js +52 -0
  9. package/dist/controller.d.ts +64 -0
  10. package/dist/controller.js +772 -0
  11. package/dist/editor.css +813 -0
  12. package/dist/engine.d.ts +2 -0
  13. package/dist/engine.js +610 -0
  14. package/dist/fake.d.ts +192 -0
  15. package/dist/fake.js +663 -0
  16. package/dist/find.d.ts +30 -0
  17. package/dist/find.js +108 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +4 -0
  20. package/dist/input.d.ts +9 -0
  21. package/dist/input.js +57 -0
  22. package/dist/keymap-table.d.ts +102 -0
  23. package/dist/keymap-table.js +130 -0
  24. package/dist/keymap.d.ts +36 -0
  25. package/dist/keymap.js +163 -0
  26. package/dist/layout.d.ts +167 -0
  27. package/dist/layout.js +583 -0
  28. package/dist/lens.d.ts +3 -0
  29. package/dist/lens.js +29 -0
  30. package/dist/listeners.d.ts +3 -0
  31. package/dist/listeners.js +12 -0
  32. package/dist/metrics.d.ts +8 -0
  33. package/dist/metrics.js +54 -0
  34. package/dist/modifier-gesture.d.ts +24 -0
  35. package/dist/modifier-gesture.js +98 -0
  36. package/dist/native-input.d.ts +26 -0
  37. package/dist/native-input.js +147 -0
  38. package/dist/offsets.d.ts +2 -0
  39. package/dist/offsets.js +14 -0
  40. package/dist/outline.d.ts +55 -0
  41. package/dist/outline.js +181 -0
  42. package/dist/overview.d.ts +17 -0
  43. package/dist/overview.js +55 -0
  44. package/dist/paint.d.ts +86 -0
  45. package/dist/paint.js +481 -0
  46. package/dist/php-html.d.ts +9 -0
  47. package/dist/php-html.js +36 -0
  48. package/dist/pointer.d.ts +49 -0
  49. package/dist/pointer.js +243 -0
  50. package/dist/scroll-animation.d.ts +3 -0
  51. package/dist/scroll-animation.js +45 -0
  52. package/dist/scroll.d.ts +28 -0
  53. package/dist/scroll.js +76 -0
  54. package/dist/semantic.d.ts +8 -0
  55. package/dist/semantic.js +47 -0
  56. package/dist/shiki.d.ts +7 -0
  57. package/dist/shiki.js +106 -0
  58. package/dist/smart-keys.d.ts +3 -0
  59. package/dist/smart-keys.js +14 -0
  60. package/dist/testing.d.ts +34 -0
  61. package/dist/testing.js +117 -0
  62. package/dist/theme-scopes.d.ts +9 -0
  63. package/dist/theme-scopes.js +61 -0
  64. package/dist/tokens.d.ts +28 -0
  65. package/dist/tokens.js +152 -0
  66. package/dist/tracked-range.d.ts +5 -0
  67. package/dist/tracked-range.js +20 -0
  68. package/dist/types.d.ts +344 -0
  69. package/dist/types.js +1 -0
  70. package/dist/view.d.ts +383 -0
  71. package/dist/view.js +2134 -0
  72. package/package.json +63 -3
  73. package/src/attribution.ts +85 -0
  74. package/src/code-block.ts +100 -0
  75. package/src/column-selection.ts +63 -0
  76. package/src/controller.ts +838 -0
  77. package/src/editor.css +813 -0
  78. package/src/engine.ts +764 -0
  79. package/src/fake.ts +838 -0
  80. package/src/find.ts +128 -0
  81. package/src/index.ts +69 -0
  82. package/src/input.ts +63 -0
  83. package/src/keymap-table.ts +163 -0
  84. package/src/keymap.ts +204 -0
  85. package/src/layout.ts +754 -0
  86. package/src/lens.ts +32 -0
  87. package/src/listeners.ts +15 -0
  88. package/src/metrics.ts +65 -0
  89. package/src/modifier-gesture.ts +115 -0
  90. package/src/native-input.ts +179 -0
  91. package/src/offsets.ts +16 -0
  92. package/src/outline.ts +248 -0
  93. package/src/overview.ts +80 -0
  94. package/src/paint.ts +608 -0
  95. package/src/php-html.ts +43 -0
  96. package/src/pointer.ts +326 -0
  97. package/src/scroll-animation.ts +48 -0
  98. package/src/scroll.ts +102 -0
  99. package/src/semantic.ts +54 -0
  100. package/src/shiki.ts +118 -0
  101. package/src/smart-keys.ts +17 -0
  102. package/src/testing.ts +157 -0
  103. package/src/theme-scopes.ts +75 -0
  104. package/src/tokens.ts +175 -0
  105. package/src/tracked-range.ts +22 -0
  106. package/src/types.ts +659 -0
  107. package/src/view.ts +2437 -0
@@ -0,0 +1,772 @@
1
+ import { replacementEdits } from './input.js';
2
+ import { chordMatches, keyAction } from './keymap.js';
3
+ import { DoubleModifierGesture } from './modifier-gesture.js';
4
+ import { NativeInput } from './native-input.js';
5
+ import { columnSelections, sameCell } from './column-selection.js';
6
+ import { PointerSelection } from './pointer.js';
7
+ const MODIFYING = new Set([
8
+ 'undo',
9
+ 'redo',
10
+ 'deleteWordLeft',
11
+ 'deleteWordRight',
12
+ 'deleteCamelLeft',
13
+ 'deleteCamelRight',
14
+ 'duplicateLine',
15
+ 'deleteLine',
16
+ 'moveLineUp',
17
+ 'moveLineDown',
18
+ 'toggleLineComment',
19
+ 'toggleBlockComment',
20
+ 'startNewLine',
21
+ 'startNewLineBefore',
22
+ 'splitLine',
23
+ 'joinLines',
24
+ 'toggleCase',
25
+ 'autoIndentLines',
26
+ 'insertTab',
27
+ 'indent',
28
+ 'outdent',
29
+ 'insertNewline',
30
+ 'smartBackspace',
31
+ 'deleteForward'
32
+ ]);
33
+ const VIEW_COMMANDS = new Set([
34
+ 'collapseRegion',
35
+ 'expandRegion',
36
+ 'collapseAllRegions',
37
+ 'expandAllRegions',
38
+ 'collapseRegionRecursively',
39
+ 'expandRegionRecursively',
40
+ 'foldSelection',
41
+ 'collapseDocComments',
42
+ 'expandDocComments',
43
+ 'expandAllToLevel1',
44
+ 'expandAllToLevel2',
45
+ 'expandAllToLevel3',
46
+ 'expandAllToLevel4',
47
+ 'expandAllToLevel5',
48
+ 'toggleColumnMode'
49
+ ]);
50
+ /* The clipboard text of the last copy or cut of a bare caret, which pastes back as whole lines. Shared, since it can be pasted in another editor. */
51
+ let lineClip = null;
52
+ /* Typing in the same stretch is one undo step until a pause this long. */
53
+ const TYPING_PAUSE_MS = 750;
54
+ const PAIR_CHARACTER = /^[()[\]{}'"`;<]$/;
55
+ /*
56
+ * What a person does to the editor: keys, typing, the clipboard, an input method, the mouse. It turns
57
+ * each into a command on the model and leaves the drawing to the view.
58
+ */
59
+ export class InputController {
60
+ native;
61
+ pointer;
62
+ cleanup = [];
63
+ keyHandlers = new Set();
64
+ gesture;
65
+ clickHandlers = new Set();
66
+ contextListeners = new Set();
67
+ subscription;
68
+ historyGroup = 0;
69
+ /* The two corners of a box being grown with the keys, and the selections it made, which say whether it is still the one on screen. */
70
+ block = null;
71
+ selectionRanges = null;
72
+ /* The Extend Selection presses that wait for a server's ranges, which are answered one after the other. */
73
+ expansions = Promise.resolve();
74
+ lastInputAt = 0;
75
+ desiredXs = [];
76
+ keepDesiredXs = false;
77
+ view;
78
+ options;
79
+ constructor(view, options) {
80
+ this.view = view;
81
+ this.options = options;
82
+ this.gesture = new DoubleModifierGesture(options.apple ? 'Alt' : 'Control');
83
+ const { model, input, viewport } = view;
84
+ this.native = new NativeInput(input, model, {
85
+ readOnly: () => view.settings.readOnly,
86
+ replace: (text) => this.replaceSelections(text),
87
+ type: (character) => this.typeText(character),
88
+ apply: (edit, selection) => {
89
+ this.applyEdits([edit], [selection]);
90
+ view.revealCaret();
91
+ },
92
+ preview: (text) => view.setComposition(text),
93
+ selectionChanged: (selection) => {
94
+ this.historyGroup++;
95
+ model.setSelections([selection]);
96
+ view.revealCaret();
97
+ }
98
+ });
99
+ this.pointer = new PointerSelection({
100
+ model,
101
+ viewport,
102
+ offsetAt: (x, y) => view.offsetAtPoint(x, y),
103
+ contentPoint: (x, y) => view.contentPoint(x, y),
104
+ columnSelections: (from, to) => (sameCell(view.layout, from, to) ? null : columnSelections(view.layout, from, to)),
105
+ visualLine: (offset) => view.layout.visualLine(offset),
106
+ lineAt: (offset) => {
107
+ const row = view.layout.rowForLine(model.positionAt(offset).line);
108
+ return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
109
+ },
110
+ camelHumps: () => view.settings.smartKeys.camelHumps,
111
+ columnMode: () => view.columnMode,
112
+ selectionAt: (x, y) => (view.settings.readOnly ? null : view.selectionAtPoint(x, y)),
113
+ setDropCaret: (offset) => view.setDropCaret(offset),
114
+ drop: (from, to, offset, copy) => this.dropText(from, to, offset, copy),
115
+ copyHeld: (event) => (this.options.apple ? event.altKey : event.ctrlKey),
116
+ focus: () => view.focus(),
117
+ scrolled: () => view.requestRender()
118
+ });
119
+ this.subscription = model.subscribe(() => {
120
+ if (!this.keepDesiredXs) {
121
+ this.desiredXs = [];
122
+ }
123
+ this.native.sync();
124
+ });
125
+ this.native.sync();
126
+ this.attach();
127
+ }
128
+ listen(target, name, callback) {
129
+ target.addEventListener(name, callback);
130
+ this.cleanup.push(() => target.removeEventListener(name, callback));
131
+ }
132
+ attach() {
133
+ const { input, viewport, root } = this.view;
134
+ const document = root.ownerDocument;
135
+ this.listen(input, 'keydown', (event) => this.keydown(event));
136
+ this.listen(input, 'beforeinput', (event) => this.beforeInput(event));
137
+ this.listen(input, 'input', () => this.native.onInput());
138
+ this.listen(input, 'compositionstart', () => {
139
+ this.historyGroup++;
140
+ this.native.startComposition();
141
+ });
142
+ this.listen(input, 'compositionend', (event) => {
143
+ this.native.endComposition(event.data);
144
+ this.historyGroup++;
145
+ this.view.render();
146
+ });
147
+ this.listen(input, 'select', () => this.native.readSelection());
148
+ this.listen(input, 'focus', () => this.view.requestRender());
149
+ this.listen(input, 'blur', () => {
150
+ this.gesture.reset();
151
+ this.view.requestRender();
152
+ });
153
+ this.listen(input, 'keyup', (event) => this.gesture.keyup(event));
154
+ this.listen(root, 'focusout', (event) => {
155
+ if (!(event.relatedTarget instanceof Node) || !root.contains(event.relatedTarget)) {
156
+ this.options.blur();
157
+ }
158
+ });
159
+ this.listen(input, 'paste', (event) => {
160
+ event.preventDefault();
161
+ this.paste(event.clipboardData?.getData('text/plain') ?? '');
162
+ });
163
+ this.listen(input, 'copy', (event) => this.copy(event, false));
164
+ this.listen(input, 'cut', (event) => this.copy(event, true));
165
+ this.listen(viewport, 'scroll', () => this.view.scrolled());
166
+ this.listen(viewport, 'wheel', () => this.view.cancelScroll());
167
+ this.listen(viewport, 'touchstart', () => this.view.cancelScroll());
168
+ this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
169
+ this.listen(viewport, 'pointerleave', () => this.view.setHover(null));
170
+ this.listen(viewport, 'pointerdown', (event) => this.pointerDown(event));
171
+ this.listen(viewport, 'contextmenu', (event) => this.contextMenu(event));
172
+ this.listen(this.view.gutterElement, 'pointerdown', (event) => this.gutterDown(event));
173
+ this.listen(this.view.stickyElement, 'pointerdown', (event) => this.stickyDown(event));
174
+ const move = (event) => this.pointer.move(event);
175
+ const end = (event) => {
176
+ this.pointer.end(event);
177
+ this.historyGroup++;
178
+ };
179
+ document.addEventListener('pointermove', move);
180
+ document.addEventListener('pointerup', end);
181
+ document.addEventListener('pointercancel', end);
182
+ this.cleanup.push(() => {
183
+ document.removeEventListener('pointermove', move);
184
+ document.removeEventListener('pointerup', end);
185
+ document.removeEventListener('pointercancel', end);
186
+ });
187
+ }
188
+ dispose() {
189
+ this.subscription.dispose();
190
+ this.pointer.dispose();
191
+ for (const remove of this.cleanup) {
192
+ remove();
193
+ }
194
+ this.cleanup.length = 0;
195
+ this.keyHandlers.clear();
196
+ }
197
+ /* Tells a person why nothing happened, when the editor is read only and has said why. */
198
+ blockedByReadOnly() {
199
+ if (!this.view.settings.readOnly) {
200
+ return false;
201
+ }
202
+ if (this.view.settings.readOnlyReason) {
203
+ this.view.notify(this.view.settings.readOnlyReason);
204
+ }
205
+ return true;
206
+ }
207
+ inputGroup() {
208
+ const now = Date.now();
209
+ if (now - this.lastInputAt > TYPING_PAUSE_MS) {
210
+ this.historyGroup++;
211
+ }
212
+ this.lastInputAt = now;
213
+ return `view-input-${this.historyGroup}`;
214
+ }
215
+ applyEdits(edits, selections) {
216
+ if (this.view.settings.readOnly) {
217
+ return false;
218
+ }
219
+ return this.view.model.applyEdits(edits, { selections, source: 'input', historyGroup: this.inputGroup() });
220
+ }
221
+ replaceSelections(text) {
222
+ if (this.blockedByReadOnly()) {
223
+ return;
224
+ }
225
+ const { model } = this.view;
226
+ for (const selection of model.getSelections()) {
227
+ this.view.ensureVisible(selection.head);
228
+ }
229
+ const replacement = replacementEdits(model.getSelections(), this.native.normalizeNewlines(text));
230
+ this.applyEdits(replacement.edits, replacement.selections);
231
+ this.view.revealCaret();
232
+ }
233
+ paste(text) {
234
+ if (this.blockedByReadOnly()) {
235
+ return;
236
+ }
237
+ const { model } = this.view;
238
+ for (const selection of model.getSelections()) {
239
+ this.view.ensureVisible(selection.head);
240
+ }
241
+ this.historyGroup++;
242
+ model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
243
+ this.historyGroup++;
244
+ this.view.revealCaret();
245
+ }
246
+ commandOptions() {
247
+ const { settings } = this.view;
248
+ const keys = settings.smartKeys;
249
+ return {
250
+ tabSize: settings.tabSize,
251
+ insertSpaces: settings.insertSpaces,
252
+ autoClosingBrackets: keys.autoPairBrackets,
253
+ autoClosingQuotes: keys.autoPairQuotes,
254
+ surroundSelection: keys.surroundSelection,
255
+ tabOutOfClosers: keys.tabOutOfClosers,
256
+ smartEnter: keys.smartIndentOnEnter,
257
+ indentOnPaste: keys.indentOnPaste,
258
+ smartSemicolon: keys.smartSemicolon,
259
+ language: settings.language ?? 'plaintext',
260
+ camelCase: keys.camelHumps,
261
+ visualLine: (offset) => this.view.rowOf(offset),
262
+ lineSpan: (line) => this.view.lineSpan(line)
263
+ };
264
+ }
265
+ /* Folding is the view's, since it knows where the ranges are. */
266
+ viewCommand(name) {
267
+ const { view } = this;
268
+ const run = {
269
+ collapseRegion: () => view.collapseRegion(),
270
+ expandRegion: () => view.expandRegion(),
271
+ collapseAllRegions: () => view.foldAll(true),
272
+ expandAllRegions: () => view.foldAll(false),
273
+ collapseRegionRecursively: () => view.collapseRecursively(),
274
+ expandRegionRecursively: () => view.expandRecursively(),
275
+ foldSelection: () => view.foldSelection(),
276
+ collapseDocComments: () => view.foldRole('doc-comment', true),
277
+ expandDocComments: () => view.foldRole('doc-comment', false),
278
+ expandAllToLevel1: () => view.expandAllToLevel(1),
279
+ expandAllToLevel2: () => view.expandAllToLevel(2),
280
+ expandAllToLevel3: () => view.expandAllToLevel(3),
281
+ expandAllToLevel4: () => view.expandAllToLevel(4),
282
+ expandAllToLevel5: () => view.expandAllToLevel(5),
283
+ toggleColumnMode: () => this.toggleColumnMode()
284
+ };
285
+ run[name]();
286
+ return true;
287
+ }
288
+ /* Runs a model command on every caret, with the folds that would hide its result opened first. */
289
+ command(name) {
290
+ if (VIEW_COMMANDS.has(name)) {
291
+ return this.viewCommand(name);
292
+ }
293
+ if (name === 'expandSelection' && this.selectionRanges !== null) {
294
+ const provider = this.selectionRanges;
295
+ this.expansions = this.expansions.then(() => this.expandThrough(provider));
296
+ return true;
297
+ }
298
+ return this.modelCommand(name);
299
+ }
300
+ setSelectionRanges(provider) {
301
+ this.selectionRanges = provider;
302
+ }
303
+ /* Extend Selection through the ranges of a language server, the editor's own rule where it has none. */
304
+ async expandThrough(provider) {
305
+ const { model } = this.view;
306
+ const selections = model.getSelections();
307
+ const revision = model.getRevision();
308
+ const stretches = selections.map(({ anchor, head }) => ({ from: Math.min(anchor, head), to: Math.max(anchor, head) }));
309
+ const asked = stretches.map(({ from }) => {
310
+ const { line, column } = model.positionAt(from);
311
+ return { line, character: column };
312
+ });
313
+ const chains = await provider(asked).catch(() => null);
314
+ if (model.getRevision() !== revision || JSON.stringify(model.getSelections()) !== JSON.stringify(selections)) {
315
+ return;
316
+ }
317
+ if (chains === null) {
318
+ this.modelCommand('expandSelection');
319
+ return;
320
+ }
321
+ const ranges = stretches.map(({ from, to }, index) => (chains[index] ?? [])
322
+ .map((range) => ({
323
+ from: model.offsetAt({ line: range.start.line, column: range.start.character }),
324
+ to: model.offsetAt({ line: range.end.line, column: range.end.character })
325
+ }))
326
+ .find((range) => range.from <= from && range.to >= to && (range.from < from || range.to > to)) ?? null);
327
+ this.historyGroup++;
328
+ model.expandSelectionTo(ranges);
329
+ this.view.revealCaret();
330
+ }
331
+ modelCommand(name) {
332
+ const { model } = this.view;
333
+ const modifies = MODIFYING.has(name) && name !== 'undo' && name !== 'redo';
334
+ if ((modifies || name === 'undo' || name === 'redo') && this.blockedByReadOnly()) {
335
+ return false;
336
+ }
337
+ this.historyGroup++;
338
+ if (modifies) {
339
+ for (const selection of model.getSelections()) {
340
+ this.view.ensureVisible(selection.head);
341
+ const position = model.positionAt(selection.head);
342
+ const line = model.getLine(position.line);
343
+ if ((name === 'smartBackspace' || name === 'deleteWordLeft') && selection.head === line.start && position.line > 0) {
344
+ this.view.ensureVisible(model.getLine(position.line - 1).start);
345
+ }
346
+ if ((name === 'deleteForward' || name === 'deleteWordRight') && selection.head === line.end && position.line + 1 < model.getLineCount()) {
347
+ this.view.ensureVisible(model.getLine(position.line + 1).start);
348
+ }
349
+ }
350
+ }
351
+ const folded = name === 'moveLineUp' || name === 'moveLineDown' ? this.view.getFolds().collapsed : [];
352
+ const result = model.execute(name, {
353
+ ...this.commandOptions(),
354
+ onLinesMoved: (moves) => this.view.refoldMoved(folded, moves)
355
+ });
356
+ if (name === 'selectNextOccurrence' && model.occurrencesExhausted && this.view.settings.messages.noMoreOccurrences !== undefined) {
357
+ this.view.notify(this.view.settings.messages.noMoreOccurrences);
358
+ }
359
+ if (name === 'smartEnd' || name === 'selectSmartEnd') {
360
+ this.view.markRowEnd(model.getPrimary().head);
361
+ }
362
+ this.view.revealCaret();
363
+ return result;
364
+ }
365
+ typeText(text) {
366
+ if (this.blockedByReadOnly()) {
367
+ return;
368
+ }
369
+ for (const selection of this.view.model.getSelections()) {
370
+ this.view.ensureVisible(selection.head);
371
+ }
372
+ if (PAIR_CHARACTER.test(text)) {
373
+ this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
374
+ this.view.revealCaret();
375
+ }
376
+ else {
377
+ this.replaceSelections(text);
378
+ }
379
+ }
380
+ /* A page scrolls whole lines, and every caret goes as far, so each stays on its row of the screen. */
381
+ movePages(direction, extend) {
382
+ const { model, layout } = this.view;
383
+ const { lineHeight } = layout.metrics;
384
+ const height = this.view.viewportHeight;
385
+ const page = Math.trunc(height / lineHeight) * lineHeight;
386
+ const from = this.view.scrollPlace;
387
+ const scrollTop = direction < 0 ? from.y - page : Math.min(layout.height - height, from.y + page);
388
+ this.view.scrollTo(from.x, Math.max(0, layout.lineBase(scrollTop)));
389
+ const distance = direction < 0 ? height : height + lineHeight - 1;
390
+ const selections = model.getSelections().map((selection, index) => {
391
+ this.desiredXs[index] ??= layout.caret(selection.head).x;
392
+ const head = layout.verticalOffset(selection.head, direction, this.desiredXs[index], distance);
393
+ return { anchor: extend ? selection.anchor : head, head };
394
+ });
395
+ this.keepDesiredXs = true;
396
+ model.setSelections(selections);
397
+ this.keepDesiredXs = false;
398
+ this.historyGroup++;
399
+ this.view.revealCaret();
400
+ }
401
+ /* Column mode on drops every caret but the first and keeps a selection as a box; off it makes one selection of the box. */
402
+ toggleColumnMode() {
403
+ const { view } = this;
404
+ const { model, layout } = view;
405
+ this.block = null;
406
+ if (view.columnMode) {
407
+ const first = model.getSelections()[0];
408
+ const last = model.getPrimary();
409
+ view.setColumnMode(false);
410
+ model.setSelections([{ anchor: first.anchor, head: last.head }]);
411
+ return;
412
+ }
413
+ view.setColumnMode(true);
414
+ const primary = model.getPrimary();
415
+ model.setSelections([primary]);
416
+ if (primary.anchor !== primary.head) {
417
+ const anchor = layout.caret(primary.anchor);
418
+ const head = layout.caret(primary.head);
419
+ this.block = { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature: '' };
420
+ this.extendBlock({ x: 0, y: 0 });
421
+ }
422
+ }
423
+ /* Moves the corner of the box by a step and selects the columns between it and the other corner. */
424
+ extendBlock(step) {
425
+ const { model, layout } = this.view;
426
+ const selections = model.getSelections();
427
+ const signature = selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
428
+ if (this.block === null || (this.block.signature !== '' && this.block.signature !== signature)) {
429
+ const primary = model.getPrimary();
430
+ const anchor = layout.caret(primary.anchor);
431
+ const head = layout.caret(primary.head);
432
+ this.block = { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature };
433
+ }
434
+ const block = this.block;
435
+ block.head = {
436
+ x: Math.max(0, block.head.x + step.x),
437
+ y: Math.max(0, Math.min(layout.height - layout.metrics.lineHeight, block.head.y + step.y))
438
+ };
439
+ model.setSelections(columnSelections(layout, block.anchor, block.head));
440
+ block.signature = model
441
+ .getSelections()
442
+ .map((selection) => `${selection.anchor}:${selection.head}`)
443
+ .join(',');
444
+ }
445
+ moveCarets(key, extend) {
446
+ if (this.view.columnMode && extend && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
447
+ const { lineHeight, charWidth } = this.view.layout.metrics;
448
+ this.extendBlock({
449
+ x: key === 'ArrowLeft' ? -charWidth : key === 'ArrowRight' ? charWidth : 0,
450
+ y: key === 'ArrowUp' ? -lineHeight : key === 'ArrowDown' ? lineHeight : 0
451
+ });
452
+ this.view.revealCaret();
453
+ return;
454
+ }
455
+ if (key === 'PageUp' || key === 'PageDown') {
456
+ this.movePages(key === 'PageUp' ? -1 : 1, extend);
457
+ return;
458
+ }
459
+ const { model, layout } = this.view;
460
+ const vertical = key === 'ArrowUp' || key === 'ArrowDown';
461
+ const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1;
462
+ const selections = model.getSelections().map((selection, index) => {
463
+ let head;
464
+ if (!vertical && !extend && selection.anchor !== selection.head) {
465
+ head = direction < 0 ? Math.min(selection.anchor, selection.head) : Math.max(selection.anchor, selection.head);
466
+ }
467
+ else if (vertical) {
468
+ // A selection first gives way to the edge in the direction of the move, as the platform's Up and Down do.
469
+ const hasSelection = !extend && selection.anchor !== selection.head;
470
+ const from = hasSelection
471
+ ? direction < 0
472
+ ? Math.min(selection.anchor, selection.head)
473
+ : Math.max(selection.anchor, selection.head)
474
+ : selection.head;
475
+ if (hasSelection) {
476
+ delete this.desiredXs[index];
477
+ }
478
+ this.desiredXs[index] ??= layout.caret(from).x;
479
+ head = layout.verticalOffset(from, direction, this.desiredXs[index]);
480
+ }
481
+ else {
482
+ head = layout.horizontalOffset(selection.head, direction);
483
+ }
484
+ return { anchor: extend ? selection.anchor : head, head };
485
+ });
486
+ this.keepDesiredXs = vertical;
487
+ model.setSelections(selections);
488
+ this.keepDesiredXs = false;
489
+ if (!vertical) {
490
+ this.desiredXs = [];
491
+ }
492
+ this.historyGroup++;
493
+ this.view.revealCaret();
494
+ }
495
+ /* Carries the text of a selection to a place in the text, or copies it there, as one step of the history, and selects it where it landed. */
496
+ dropText(from, to, offset, copy) {
497
+ if (this.blockedByReadOnly()) {
498
+ return;
499
+ }
500
+ const { model } = this.view;
501
+ const text = model.slice(from, to);
502
+ this.view.ensureVisible(offset);
503
+ const start = copy || offset < from ? offset : offset - (to - from);
504
+ this.historyGroup++;
505
+ model.applyEdits(copy
506
+ ? [{ from: offset, to: offset, text }]
507
+ : [
508
+ { from, to, text: '' },
509
+ { from: offset, to: offset, text }
510
+ ], {
511
+ source: 'input',
512
+ selections: [{ anchor: start, head: start + text.length }]
513
+ });
514
+ this.historyGroup++;
515
+ this.view.revealCaret();
516
+ }
517
+ goToMatchingBracket() {
518
+ const { model } = this.view;
519
+ let changed = false;
520
+ model.setSelections(model.getSelections().map((selection) => {
521
+ const bracket = this.view.matchingBracket(selection);
522
+ if (!bracket || bracket.mate === undefined) {
523
+ return selection;
524
+ }
525
+ const head = bracket.mate > bracket.at ? bracket.mate + 1 : bracket.mate;
526
+ changed = true;
527
+ return { anchor: head, head };
528
+ }));
529
+ if (changed) {
530
+ this.view.revealCaret();
531
+ }
532
+ }
533
+ run(action, event) {
534
+ const { model } = this.view;
535
+ switch (action.type) {
536
+ case 'save':
537
+ event.preventDefault();
538
+ this.options.save();
539
+ return;
540
+ case 'command':
541
+ event.preventDefault();
542
+ this.command(action.command);
543
+ return;
544
+ case 'move':
545
+ event.preventDefault();
546
+ this.moveCarets(action.key, action.extend);
547
+ return;
548
+ case 'edge': {
549
+ event.preventDefault();
550
+ const head = action.end ? model.getLength() : 0;
551
+ model.setSelections([{ anchor: action.extend ? model.getPrimary().anchor : head, head }]);
552
+ this.view.revealCaret();
553
+ return;
554
+ }
555
+ case 'bracket':
556
+ event.preventDefault();
557
+ this.goToMatchingBracket();
558
+ return;
559
+ case 'view':
560
+ event.preventDefault();
561
+ this.viewCommand(action.command);
562
+ return;
563
+ case 'escape': {
564
+ const selections = model.getSelections();
565
+ if (selections.length > 1 || selections[0].anchor !== selections[0].head) {
566
+ event.preventDefault();
567
+ // The oldest caret stays; the newest ones go.
568
+ const head = selections[0].head;
569
+ model.setSelections([{ anchor: head, head }]);
570
+ if (selections.length > 1) {
571
+ this.view.revealCaret();
572
+ }
573
+ }
574
+ return;
575
+ }
576
+ }
577
+ }
578
+ /* A handler sees the key before the editor does, the page's own shortcuts included, since a list of suggestions owns the arrows while it is open. */
579
+ onKeyDown(handler) {
580
+ this.keyHandlers.add(handler);
581
+ return () => {
582
+ this.keyHandlers.delete(handler);
583
+ };
584
+ }
585
+ onClick(handler) {
586
+ this.clickHandlers.add(handler);
587
+ return () => {
588
+ this.clickHandlers.delete(handler);
589
+ };
590
+ }
591
+ onContextMenu(listener) {
592
+ this.contextListeners.add(listener);
593
+ return () => {
594
+ this.contextListeners.delete(listener);
595
+ };
596
+ }
597
+ keydown(event) {
598
+ if (event.isComposing || this.native.composing) {
599
+ return;
600
+ }
601
+ const added = this.gesture.keydown(event);
602
+ if (added !== null) {
603
+ event.preventDefault();
604
+ this.command(added === 'above' ? 'addCaretAbove' : 'addCaretBelow');
605
+ return;
606
+ }
607
+ for (const handler of [...this.keyHandlers]) {
608
+ if (handler(event)) {
609
+ event.preventDefault();
610
+ return;
611
+ }
612
+ }
613
+ // The app's own shortcuts stay the app's, also where the editor has a binding for the same key.
614
+ if (this.options.handBack.some((chord) => chordMatches(chord, event, this.options.apple))) {
615
+ return;
616
+ }
617
+ const action = keyAction(event, this.options.apple, this.options.keymap);
618
+ if (action) {
619
+ this.run(action, event);
620
+ }
621
+ }
622
+ beforeInput(event) {
623
+ if (this.view.settings.readOnly) {
624
+ event.preventDefault();
625
+ this.blockedByReadOnly();
626
+ return;
627
+ }
628
+ if (this.native.composing || event.isComposing) {
629
+ return;
630
+ }
631
+ switch (event.inputType) {
632
+ case 'insertText':
633
+ case 'insertReplacementText':
634
+ event.preventDefault();
635
+ if (event.data !== null) {
636
+ this.typeText(event.data);
637
+ }
638
+ return;
639
+ case 'insertLineBreak':
640
+ case 'insertParagraph':
641
+ event.preventDefault();
642
+ this.command('insertNewline');
643
+ return;
644
+ case 'deleteContentBackward':
645
+ event.preventDefault();
646
+ this.command('smartBackspace');
647
+ return;
648
+ case 'deleteContentForward':
649
+ event.preventDefault();
650
+ this.command('deleteForward');
651
+ return;
652
+ case 'historyUndo':
653
+ case 'historyRedo':
654
+ event.preventDefault();
655
+ this.command(event.inputType === 'historyUndo' ? 'undo' : 'redo');
656
+ return;
657
+ }
658
+ }
659
+ /* A cut or copy of nothing selected takes the lines the carets are on. */
660
+ copy(event, cut) {
661
+ event.preventDefault();
662
+ const { model } = this.view;
663
+ const selections = model.getSelections();
664
+ const empty = selections.every((selection) => selection.anchor === selection.head);
665
+ const text = empty
666
+ ? selections
667
+ .map((selection) => {
668
+ const { first, last } = this.view.lineSpan(model.positionAt(selection.head).line);
669
+ return { start: model.getLine(first).start, end: model.getLine(last).end, next: model.getLine(last).next };
670
+ })
671
+ .map((lines) => model.slice(lines.start, lines.next) + (lines.next === lines.end ? '\n' : ''))
672
+ .join('')
673
+ : selections.map((selection) => model.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head))).join('\n');
674
+ event.clipboardData?.setData('text/plain', text);
675
+ lineClip = empty ? text : null;
676
+ if (!cut || this.blockedByReadOnly()) {
677
+ return;
678
+ }
679
+ this.historyGroup++;
680
+ if (empty) {
681
+ this.command('deleteLine');
682
+ }
683
+ else {
684
+ this.replaceSelections('');
685
+ }
686
+ this.historyGroup++;
687
+ }
688
+ pointerDown(event) {
689
+ this.view.cancelScroll();
690
+ const target = event.target;
691
+ if (event.button !== 0 || target.closest('button, a, input, textarea, [contenteditable], .se-block, .se-gutter, .se-sticky')) {
692
+ return;
693
+ }
694
+ event.preventDefault();
695
+ if (this.takenByHost(event)) {
696
+ this.view.focus();
697
+ return;
698
+ }
699
+ this.pointer.start(event);
700
+ }
701
+ /* Whether a handler of the host took a press on a character, such as a Mod+click that follows a name. */
702
+ takenByHost(event) {
703
+ if (this.clickHandlers.size === 0) {
704
+ return false;
705
+ }
706
+ const offset = this.view.characterAtPoint(event.clientX, event.clientY, event.target);
707
+ if (offset === null) {
708
+ return false;
709
+ }
710
+ const click = {
711
+ position: this.positionOf(offset),
712
+ mod: this.options.apple ? event.metaKey : event.ctrlKey,
713
+ alt: event.altKey,
714
+ shift: event.shiftKey
715
+ };
716
+ return [...this.clickHandlers].some((handler) => handler(click));
717
+ }
718
+ positionOf(offset) {
719
+ const { line, column } = this.view.model.positionAt(offset);
720
+ return { line, character: column };
721
+ }
722
+ contextMenu(event) {
723
+ if (this.contextListeners.size === 0 || event.target?.closest?.('.se-sticky')) {
724
+ return;
725
+ }
726
+ event.preventDefault();
727
+ const offset = this.view.offsetAtPoint(event.clientX, event.clientY);
728
+ const selection = this.view.model
729
+ .getSelections()
730
+ .some((candidate) => Math.min(candidate.anchor, candidate.head) <= offset && offset <= Math.max(candidate.anchor, candidate.head));
731
+ const menu = { position: this.positionOf(offset), inSelection: selection, x: event.clientX, y: event.clientY };
732
+ for (const listener of [...this.contextListeners]) {
733
+ listener(menu);
734
+ }
735
+ }
736
+ /* A press on a pinned header goes to that header. */
737
+ stickyDown(event) {
738
+ const row = event.target?.closest?.('.se-sticky-row');
739
+ if (event.button !== 0 || !row) {
740
+ return;
741
+ }
742
+ event.preventDefault();
743
+ this.view.jumpToHeader(Number(row.dataset.line), Number(row.dataset.depth));
744
+ this.view.focus();
745
+ }
746
+ /* A press on a line number selects the line. */
747
+ gutterDown(event) {
748
+ if (event.button !== 0) {
749
+ return;
750
+ }
751
+ event.preventDefault();
752
+ /* On the press and not on click: a repaint in between replaces the gutter's buttons, and a click whose button left the DOM never arrives. */
753
+ const markerId = this.view.gutterMarkerIdOf(event.target);
754
+ if (markerId !== null) {
755
+ this.view.pressGutterMarker(markerId);
756
+ return;
757
+ }
758
+ const actionLine = this.view.gutterActionLineOf(event.target);
759
+ if (actionLine !== null) {
760
+ this.view.pressGutterAction(actionLine);
761
+ return;
762
+ }
763
+ const foldLine = this.view.foldLineOf(event.target);
764
+ if (foldLine !== null) {
765
+ this.view.toggleFold(foldLine);
766
+ this.view.focus();
767
+ return;
768
+ }
769
+ this.pointer.start(event, 'gutter');
770
+ this.view.focus();
771
+ }
772
+ }